When Next.js moved from Pages Router to App Router, next-intl quickly became the community default. Jan Amann did solid work maintaining it, and for most teams building multilingual sites, it was the obvious choice.
I’ve been building and profiling internationalized Next.js apps for a long time. Over the last couple of years, the rest of the React ecosystem shifted hard toward server components and compile-time optimizations. But when you inspect what next-intl actually delivers to the browser in production, a few architectural bottlenecks become obvious.
The biggest issue isn't even the runtime footprint (though loading ICU formatters and client providers adds around 12 KB gzipped before rendering any text). The real issue is copy leakage across pages.
In most setups, developers mount NextIntlClientProvider in the root layout with getMessages(). The result: visiting a simple /contact page forces the browser to download translations for /dashboard, /pricing, /settings, and your entire app. When we benchmarked a standard 10-route setup, nearly 90% of the translation payload sent to a single page belonged to other routes.
On top of that, because t("key") evaluates string keys dynamically at runtime, Turbopack and Webpack have no way of knowing which strings you actually use. As a result, bundlers can't tree-shake unused keys out of your client chunks.
Then there is the DX friction around Server Components. To make static rendering (SSG) work on the App Router, next-intl had to patch in the setRequestLocale(locale) boilerplate (formerly unstable_setRequestLocale). You are forced to manually call it at the top of every single layout, sub-layout, and page. If you forget it in just one place, Next.js silently opts that route out of static rendering into dynamic server rendering. Worse, when building reusable client components or a shared design system, there is no synchronous way to access translations—you're stuck either wrapping everything in context providers or manually prop-drilling locale through every layer.
How do you optimize this setup?
If you run next-intl in production today, you have a few ways to address these issues.
First, stop passing all messages at the root layout. You can split your JSON files into route-specific namespaces and only pass relevant keys via getMessages() inside each page or sub-layout. It takes manual work to keep page-to-namespace mappings up to date without missing keys, but it immediately cuts down bundle leakage.
Second, push translations to React Server Components wherever possible. If you render text using getTranslations() inside server components instead of useTranslations() in client components, those strings stay on the server as plain HTML. Zero client runtime cost, zero hydration overhead.
Third, look into compile-time extraction. The modern direction for i18n is moving away from shipping raw JSON dictionaries to the browser altogether. Tools like Intlayer analyze your imports during the build, prune unused keys per route via static analysis, and eliminate cross-page leakage without manual namespace bookkeeping. It also removes the need for setRequestLocale boilerplate or prop-drilling across design systems. If you already have a large codebase, there is a drop-in compatibility package (@intlayer/next-intl) that aliases your existing useTranslations calls so you get compiler-level tree-shaking without rewriting your components.
If you’re running a multilingual Next.js app in production, inspect your network tab and check how much unused copy is getting shipped on initial load. You might find some easy wins.
You can read the full write-up with our detailed benchmark data and methodology here:
https://intlayer.org/blog/is-next-intl-outdated

Top comments (0)