Когда Next.js перешел с Pages Router на App Router и убрал встроенную поддержку i18n‑роутинга, библиотека next-intl стала стандартом по умолчанию для большинства команд. Ян Аманн проделал отличную работу по ее поддержке, и долгое время это был самый надежный выбор для мультиязычных проектов.

Я давно занимаюсь профилированием производительности интернационализированных приложений на Next.js. За последние годы вся React‑экосистема заметно сместилась в сторону серверных компонентов (RSC) и оптимизаций на этапе компиляции. Но если заглянуть под капот того, что next-intl реально отдает в браузер на проде, становятся видны явные архитектурные узкие места.
Главная проблема даже не в весе самого рантайма (хотя провайдер и парсер ICU отъедают около 12 КБ gzipped еще до показа первого слова). Главная беда — это утечка переводов между страницами.
В подавляющем большинстве проектов NextIntlClientProvider подключается в корневом лейауте вместе с getMessages(). В итоге пользователь открывает обычную страницу /contact, а браузер послушно выкачивает тексты для /dashboard, /pricing, настроек и вообще всего приложения. В нашем тесте на стандартном проекте из 10 страниц около 90% веса переводов, пришедших на конкретный роут, относились к совершенно другим экранам.
Кроме того, вызов t("key") динамически вычисляет строку во время выполнения. Из‑за этого ни Webpack, ни Turbopack не могут понять, какие именно ключи используются, и не могут вырезать лишние строки через Tree‑shaking.
Отдельная боль — костыли вокруг серверных компонентов (RSC). Чтобы заставить работать статическую генерацию (SSG) в App Router, разработчикам next-intl пришлось внедрить заплатку setRequestLocale(locale) (ранее unstable_setRequestLocale). Ее приходится вручную прописывать в самом начале каждого лейаута, подлейаута и страницы. Забудете хоть в одном месте — Next.js молча откажется от статики и переключит роут на динамический рендеринг при каждом запросе. Хуже того, в клиентских компонентах и переиспользуемых дизайн‑системах нет нормального синхронного способа получить перевод: приходится либо оборачивать всё в контекст‑провайдеры, либо вручную пробрасывать locale пропсами через все слои дерева компонентов (prop‑drilling).
Как оптимизировать такое решение?
Если вы уже используете next-intl на проде, есть несколько рабочих шагов, чтобы привести бандл в порядок.
Во‑первых, откажитесь от загрузки всех переводов в корневом лейауте. Разбейте JSON‑файлы на неймспейсы по конкретным роутам и вызывайте getMessages() локально на уровне страниц или вложенных лейаутов. Да, ручной маппинг страниц и неймспейсов требует дисциплины, чтобы случайно не потерять ключ, но это сразу убирает тонны чужого текста из чанков.
Во‑вторых, выносите переводы в React Server Components везде, где это возможно. Если отрисовывать статичный текст через getTranslations() на сервере, а не через useTranslations() в клиентских компонентах, строки уходят в браузер в виде чистого HTML. Это дает ноль оверхеда на рантайм и ноль лишней нагрузки на гидратацию.
В‑третьих, стоит присмотреться к статической экстракции на этапе сборки. Современный i18n постепенно уходит от передачи сырых JSON‑словарей клиенту. Решения вроде Intlayer анализируют импорты компонентов при сборке, автоматически отсекают неиспользуемые ключи для каждого роута и избавляют от ручной возни с неймспейсами, костылей вроде setRequestLocale и проброса пропсов в дизайн‑системах. Для существующих кодовых баз есть слой совместимости @intlayer/next-intl, который подменяет алиасы и дает тришейкинг на уровне компилятора без необходимости переписывать вызовы useTranslations.
Если у вас крутится мультиязычный Next.js на проде, откройте Network во вкладке разработчика и проверьте, сколько неиспользуемого текста улетает пользователю на первом экране. Там часто скрываются очень легкие победы по оптимизации.
Подробные замеры, методологию бенчмарков и цифры можно посмотреть в полной статье:

