next-intl icon indicating copy to clipboard operation
next-intl copied to clipboard

Prepare for `ppr` and `dynamicIO`

Open amannn opened this issue 1 year ago • 10 comments

Next.js is changing the rendering behavior. It's still very early, and things are moving, but it would be interesting to see if we can adapt next-intl to better suit the needs of these rendering modes.

For now, this issue is meant to collect related resources.

Related

amannn avatar Oct 31 '24 06:10 amannn

Hi @amannn, thank you for the great work on next-intl!

Just to confirm, does next-intl currently not support ~per-page revalidation~ partial prerendering (PPR) for Next 15? Since params are now asynchronous, it seems we need to do something like this:

export default async function Layout(
  props: { params: Promise<{locale: string}> }
) {
  const params = await props.params;
  setRequestLocale(params.locale);

Would this approach opt out of static rendering?

zipme avatar Nov 06 '24 15:11 zipme

Hmm, are we talking about the same thing? PPR in Next.js refers to partial prerendering, but based on your comment I think you mean the same thing?

I haven't had the time to experiment with PPR yet, but am planning to do this in the next few days. I'd be curious if generateStaticParams has an effect on whether reading params requires dynamic rendering—at least from a user perspective I'd expect it to render fine statically. Not sure if other pieces are relevant though like 'use cache';.

I'll update this thread here with my findings!

amannn avatar Nov 07 '24 08:11 amannn

Thanks for the update! Yes, I was referring to partial prerendering (I updated my comment). I would have expected generateStaticParams to influence this behavior, and it indeed worked in Next 14. However, after upgrading to 15, it no longer functions as expected.

It's still unclear whether params qualifies as a dynamic API. It's not listed here: https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-apis, but in the version 15 upgrade guide, it does mention an async request dynamic API change, and params appears in the list.

zipme avatar Nov 07 '24 09:11 zipme

That's interesting, yes. There seems to be a new API coming for accessing top-level params: https://github.com/amannn/next-intl/issues/663#issuecomment-2449100558. Maybe that one will have a different behavior when used with PPR.

amannn avatar Nov 07 '24 09:11 amannn

Ok, I was heads-down exploring implications of dynamicIO and ppr for next-intl over the last few days.

Two insights led to minor changes for the upcoming next-intl@4:

  1. https://github.com/amannn/next-intl/pull/1536
  2. https://github.com/amannn/next-intl/pull/1541

Apart from this, my impression was that ppr + dynamicIO have some rather rough edges currently. That being said, I hope the changes mentioned above cover all potential adaptions relevant for next-intl, so when the development on the Next.js side progresses, users should be able to upgrade without any changes in next-intl.

I made a few notes during my exploration, but it mostly comes down to apps with i18n routing requiring these currently missing pieces:

  1. A replacement for dynamicParams = false
  2. An API to read params deeply: rootParams

For apps without i18n routing, I think next-intl@4 and the patterns suggested in the docs might work.

amannn avatar Nov 14 '24 14:11 amannn

Thanks for the update! Yes, I was referring to partial prerendering (I updated my comment). I would have expected generateStaticParams to influence this behavior, and it indeed worked in Next 14. However, after upgrading to 15, it no longer functions as expected.

It's still unclear whether params qualifies as a dynamic API. It's not listed here: https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-apis, but in the version 15 upgrade guide, it does mention an async request dynamic API change, and params appears in the list.

I believe, anything that is async is considere dynamic under dynamicIO

dBianchii avatar Dec 10 '24 02:12 dBianchii

would like to see a i18n routing example working with staticly generated pages using dynamicIO

dBianchii avatar Dec 10 '24 02:12 dBianchii

I think rootParams might be required for this (example test).

amannn avatar Dec 10 '24 07:12 amannn

Hi, @amannn! I'm trying to use dynamicIO in a project along with useCache, but now all pages become dynamic, even with i18 routing. Is this the expected behavior? Using next-intl 4

andrewLapshov avatar Apr 18 '25 08:04 andrewLapshov

Yes, rootParams will be required for static rendering with i18n routing. I found that setRequestLocale will not work with dynamicIO. It should be easy to adopt based on my experiments, but for the time being I guess we have to wait.

amannn avatar Apr 18 '25 09:04 amannn

Hey @amannn, I used Next canary to see if I could do PPR and turn most of my page static but next-intl was failing and so my attempt wasn't successful. I need PPR to wrap my dynamic Shopping Bag (cart).

I did not use dynamicIO though. Is this issue related to PRR even without dynamicIO on? Do we need rootParams in this case too?

Using next-intl 4. Thanks :)

MichalMoravik avatar Jul 04 '25 14:07 MichalMoravik

Hey @MichalMoravik, if you're using i18n routing, you indeed need rootParams for PPR (see my comments above). It's currently available via unstable_rootParams, but it seems like the API will change a bit in the near future: https://github.com/vercel/next.js/pull/80255. So it might be best to wait a bit here.

amannn avatar Jul 07 '25 06:07 amannn

Alright, yea, you're right. Thank you :)

MichalMoravik avatar Jul 07 '25 07:07 MichalMoravik

DynamicIO has been renamed to cacheComponents. Please update the title to avoid confusion :))

JakubFaltyn avatar Jul 18 '25 15:07 JakubFaltyn

DynamicIO has been renamed to cacheComponents. Please update the title to avoid confusion :))

From https://nextjs.org/blog/next-15-4

Cache Components (beta): Consolidates experimental caching features (Dynamic IO, use cache, and Partial Prerendering) into a unified cacheComponents flag, simplifying performance optimization strategies. The Next.js 16 blog post will detail our vision.

mtlaso avatar Jul 24 '25 02:07 mtlaso

For apps without i18n routing, I think next-intl@4 and the patterns suggested in the docs might work.

Hey @amannn. First of all thanks for the great work! 💪

I was trying to use next-intl@4 with the latest nextjs@canary version, without i18n routing. Docs suggest to do the following in the root layout

import {NextIntlClientProvider} from 'next-intl';
import {getLocale} from 'next-intl/server';
 
export default async function RootLayout({
  children
}: {
  children: React.ReactNode;
}) {
  const locale = await getLocale();
 
  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider>{children}</NextIntlClientProvider>
      </body>
    </html>
  );
}

However, having the await triggers a warning/error in Next.js: A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense. In this case I can't really use rootParams, since as I mentioned, I'm not using i18n routing.

Any suggestion?

FacundoSpira avatar Aug 14 '25 02:08 FacundoSpira

@FacundoSpira Thanks for chiming in here! I guess in your i18n/request.ts you have some kind of dynamic function which fetches the user locale, right? That's likely triggering the warning.

Based on https://github.com/vercel/next.js/discussions/71927#discussioncomment-11105573, I think this will be supported in the future:

It will also be possible to use a Suspense boundary above html but that's not the best solution since it makes the whole page dynamic.

Making the whole page dynamic is essentially what you need with this approach, since the very first piece of your HTML markup (<html lang="…">) depends on dynamic user information.

Potentially to get rid of the warning, can you do something like this?

export default function RootLayout({children}) {
  return (
    <Suspense fallback={<html lang="en"><body>Loading …</body></html>}>
      <AppLayout />
    </Suspense>
  );
}

async function AppLayout() {
  const locale = await getLocale();
 
  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider>{children}</NextIntlClientProvider>
      </body>
    </html>
  );
}

I haven't tried it, but maybe worth a shot. Otherwise this might require future improvements in Next.js as mentioned in the linked comment.

But if your whole page relies on request-specific user information, I guess PPR can't prerender too much anyway. I'd guess some strategic 'use cache' for certain components can be more useful here (esp. for non-user specific parts).

amannn avatar Aug 14 '25 09:08 amannn

FYI rootParams seems to have been removed : https://github.com/vercel/next.js/pull/84373

LouisCuvelier avatar Oct 06 '25 12:10 LouisCuvelier

FYI rootParams seems to have been removed : vercel/next.js#84373

Or likely stabilized, as the unstable_rootParams was removed in favor of compile-time insert, hence they're cleaning up old explicit usage in favor of helper getRootParam: https://github.com/vercel/next.js/blob/4c4a5bc83f690a7248068f05e4d285a4d42e07a0/packages/next/src/build/webpack/loaders/next-root-params-loader.ts#L37

asgorobets avatar Oct 06 '25 17:10 asgorobets

Related to using cacheComponents, I've opened a discussion over in the Next.js repository to get dynamicParams = false working for this mode: https://github.com/vercel/next.js/discussions/84991

If you're interested in this, please leave an upvote on the discussion and feel free to join the conversation!

amannn avatar Oct 17 '25 08:10 amannn

Since cacheComponents is available as stable in Next.js 16, the question for support with next-intl came up again.

The tldr; is that cacheComponents is not supported yet with the capabilities we have today.

But note: Next.js 16 by itself (if you don't use cacheComponents) is supported as of [email protected].


Longer version:

What we need is next/root-params. It's expected to ship in a Next.js 16.x minor version based on the Next.js 16 release blog post.

An experimental version of next/root-params is already available in the latest Next.js versions, but in its current state, it can't be used within a 'use cache' boundary.

The error message you'll see hints at support being added in the future though:

Route /[locale] used `import('next/root-params').locale()` inside `"use cache"` or `unstable_cache`.
Support for this API inside cache scopes is planned for a future version of Next.js.

So for the time being, it seems like we still have to wait a bit …

A bit more on the necessity for next/root-params:

The mechanism for passing a locale to Server Components that's currently used in next-intl is really a workaround that glued things together until something like next/root-params ships.

If you don't use static rendering, Next.js will complain about a header being read in cached content. And if you use setRequestLocale, then you might see cases where the locale is randomly lost. Either isn't a good fit for cacheComponents.

The good news is that next-intl@4 will work with next/root-params without a library change being necessary, so everything is already in place. I'll put a blog post out with guidance on how to use it as soon as there's more news regarding this feature! 🙂

See also: Reading params deeply in Server Components (e.g. for i18n / multi-tenancy)

Temporary workaround:

If you enable cacheComponents but don't use 'use cache' anywhere, then your app might still work. It will break as soon as you add 'use cache' somewhere.

However, you can theoretically work around this if you pass an explicit locale to APIs like await getTranslations.

It might be easy to miss though, so next/root-params should really be the right fix here.

amannn avatar Oct 23 '25 09:10 amannn

I'm uncertain whether next/root-params will be supported in server actions in addition to 'use cache.' The current unstable_rootParams API does support server actions. If root parameters are not supported, will we need to pass the locale to the server action as well?

For example

const locale = useLocale()
const someHandler = () => {
  startTransition(() => {
   await someServerAction({ locale })
  })
}

zipme avatar Oct 23 '25 09:10 zipme

@zipme Yep, that's still up for debate as far as I know. What makes Server Actions a bit special is that they're put in a queue and by the time they execute, the root param value (or even segment) could be different. I'd certainly really appreciate it if Server Actions were supported.

I hope there can be more public discourse about this once the feature is mentioned in the Next.js docs.

But yeah, the alternative is passing the locale to the action manually and consuming it via getTranslations({locale}).

amannn avatar Oct 23 '25 09:10 amannn

Yeah, I noticed passing setRequestLocale seems unreliable when using use cache with other dynamic params.

For example, I tried use cache on a route like this: /[locale]/posts/[id]

When I visited that page for the first time, it worked fine: /en/posts/1

However, it broke when I changed the locale to /pt/posts/1. It was using the previous locale (en)

A workaround for this seems to be returning returning at least one locale from generateStaticParams for dynamic pages

When I did this, it worked:

export function generateStaticParams() {
  return [{ id: "1", locale: "en" }];
}

I've created a repo where I was playing with cache components and next-intl. It does seem to be working with "use cache"

That being said, despite cacheComponents being stable, it still has some weird issues. For example, they introduced this weird bug on canary.17 the night before launching Next.js and making it stable

So, I'd be cautious using cacheComponents right now for something in production. I'd wait for 16.1 or 16.2 unless you're starting a new project and can afford some bugs

ceolinwill avatar Oct 23 '25 13:10 ceolinwill

Will Next-Intl + cacheComponents work without blocking the route (Blocking Route Error: Uncached data was accessed outside of <Suspense>) without using locale based routing? Because the suggested fix uses next/root-params which reads out the lang parameter.

Joshua-hypt avatar Oct 23 '25 13:10 Joshua-hypt

@Joshua-hypt How are you reading the locale in getRequestConfig? E.g. if you're reading from cookies, it's naturally a bit hard to cache on the server since it depends on the user request. It seems like something like use cache: private aims to cover cases that include cookies for example, so this could be something to investigate. Note that use cache: 'private' is experimental at this point.

If you look into this, please share your experience here!

amannn avatar Oct 23 '25 13:10 amannn

@amannn I don't think there is a non-blocking/static way to fetch the locale inside getRequestConfig without using locale based routing (correct me if I'm wrong). As far as I understand, use cache: private allows for runtime prefetching while using cookies/headers, but on initial render it will still be blocked, requiring a suspense >.<

So since cookies() are async now, it wouldn't be possible?

Joshua-hypt avatar Oct 23 '25 15:10 Joshua-hypt

but on initial render it will still be blocked, requiring a suspense

I think you're right, yes! But it kind of makes sense, right? If you only know the locale of the user once a request hits the server, that's the time you can start rendering with it, right? If you need it to set the <html lang="…"> attribute, that effectively means that blocking the entire page would be necessary.

However, I guess as your supported locales are a limited set of values, another option could be to use localePrefix: 'never' for this use case, which can help you cache all localized variants while picking the right one with a lightweight ~~middleware~~ proxy that reads the cookie.

Maybe that could be something for your use case?

amannn avatar Oct 23 '25 15:10 amannn

I came up with something like this for my personal needs.

import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Suspense } from 'react';
import { Skeleton } from '@mui/material';

export const getTranslationsPromise = async ({
  locale,
  namespace,
}: {  
  locale: Promise<string>;
  namespace: string;
}) => {
  const localeString = await locale;
  const t = await getTranslations({
    locale: localeString,
    namespace,
  });
  setRequestLocale(localeString);
  return t;
};
 
const AsyncTranslatorBlockingImpl = async ({
  translations,
  translationKey,
}: {
  translations: Promise<(key: string) => string>;
  translationKey: string;
}) => {
  const t = await translations;
  return t(translationKey);
};

export const AsyncTranslator = ({translations, translationKey}: {
  translations: Promise<(key: string) => string>,
  translationKey: string,
}) => {
  return (
    <Suspense fallback={<Skeleton variant="text" width={100} height={24} />}>
       <AsyncTranslatorBlockingImpl translations={translations} translationKey={translationKey} />
    </Suspense>
  );
}

export const getTranslationFn = (translations: Promise<(key: string) => string>) => {
  // eslint-disable-next-line react/display-name
  return (translationKey: string) => <AsyncTranslator translations={translations} translationKey={translationKey} />;
}

Usage is pretty simple

export default async function AboutPage({
  params,
}: {
  params: Promise<{ locale: string }>;
}) {
  const locale = params.then(({ locale }) => locale);
  const translations = getTranslationsPromise({ locale, namespace: 'About' });
  const t = getTranslationFn(translations);

return (
  <Button
       .....
  >
    {t('telegram')}
  </Button>
)

The whole route is still kinda blocked on server answer. As I had to put NextIntlClientProvider inside a Suspense, but the page is much more responsive. I tested this on NextJS 15.5.1-canary.39 (with cacheComponents = true, reactCompiler = true), because the 16.0.0 has some issues with Material UI.

It's okay, because it's a small scale personal project.

In-line avatar Oct 25 '25 12:10 In-line

To work with cacheComponent, my current approach is to convert everything into async components (because use cache can only be used in async functions), then get the locale from params and set it using setRequestLocale. Otherwise, all related components afterward would request the cookie(or header?), which means they all need to be wrapped with Suspense.

Places where Link(from next-intl) is used also need to be wrapped with Suspense, preferably with an additional wrapper.

fairjm avatar Oct 30 '25 14:10 fairjm