Автор:
    Создание:2026-09-13Последнее обновление:2026-09-13

    next-intl VS Intlayer | Benchmark интернационализации Next.js (i18n)

    next-intl - самая популярная библиотека i18n для Next.js. Intlayer - это альтернатива на основе компилятора с областью видимости компонента. Обе локализуют приложение App Router. Вопрос в том, какие затраты каждая из них влечет после сборки приложения.

    Эта статья - не учебное пособие. Это сравнение, подкрепленное числами из Benchmark Bloom, открытого набора инструментов для тестирования, который создает одно и то же приложение с каждой библиотекой и измеряет то, что браузер фактически загружает и выполняет.

    tl;dr: На одном и том же приложении Next.js next-intl добавляет +12.6 KB gzip JavaScript на каждую страницу, против +0.3 KB для Intlayer. Без дополнительной работы next-intl поставляет ~90% строк иностранных страниц с каждой страницей. Чтобы достичь утечки 0% с next-intl, требуется область видимости namespace и per-page pick(messages, [...]). Intlayer достигает 0% по умолчанию, потому что его компилятор ограничивает контент для каждого компонента. Если вы хотите API next-intl с выходом Intlayer, адаптер @intlayer/next-intl показал 147.5 KB на страницу против 153.6 KB с оригиналом.

    Вкратце

    • next-intl - Легковесная, хорошо документированная, поддержка ICU message format, встроенная поддержка App Router с middleware, форматировщиками и помощниками навигации. Контент хранится в централизованных JSON каталогах; оптимизация производительности (namespaces, выборка сообщений для каждой страницы, ленивая загрузка) - ваша ответственность.
    • Intlayer - Компонентоцентричная модель контента. Словари .content.ts находятся рядом с компонентом, который они обслуживают, compiler во время сборки выполняет tree-shaking и ленивую загрузку для каждого компонента и локали, строгие типы TypeScript генерируются из вашего контента, и отсутствующие переводы вызывают ошибку во время сборки. Поставляется с middleware, помощниками SEO, Visual Editor / CMS и переводом с поддержкой AI.
    БиблиотекаGitHub StarsВсего коммитовПоследний коммитПервая версияNPM версияЗагрузки NPM
    aymericzip/intlayerGitHub Repo starsGitHub commit activityLast CommitАпрель 2024npmnpm downloads
    amannn/next-intlGitHub Repo starsGitHub commit activityLast CommitNov 2020npmnpm downloads
    Значки обновляются автоматически. Снимки экрана будут изменяться с течением времени.

    Сравнение функций "рядом"

    Функцияnext-intlayer (Intlayer)next-intl
    Переводы рядом с компонентами✅ Да, .content.ts расположен вместе с каждым компонентом❌ Нет, централизованный messages/{locale}.json
    Интеграция TypeScript✅ Строгие типы автоматически генерируются из контента✅ Хорошо, типизация ключей через расширение global.d.ts
    Обнаружение отсутствующих переводов✅ Ошибка TypeScript + ошибка/предупреждение на этапе сборки⚠️ Fallback во время выполнения + предупреждение консоли
    Rich content (JSX / Markdown / components)✅ Прямая поддержка⚠️ t.rich() / t.markup() с заполнителями тегов
    ICU support⚠️ WIP✅ Да
    Formatting (dates, numbers, currencies)useNumber, useDate, ... (Intl под капотом)useFormatter() (Intl под капотом)
    Локализованная маршрутизация и middleware✅ Встроенный прокси/middleware, getMultilingualUrls✅ Встроенный middleware, Link, redirect, usePathname
    SEO помощники (hreflang, sitemap, robots)✅ Встроенные помощники⚠️ Ручной, на основе конфигурации маршрутизации
    Синхронные серверные компонентыuseIntlayer из next-intlayer/server работает в любом дочернем серверном компоненте⚠️ getTranslations асинхронен; синхронные дочерние компоненты нуждаются в t переданном как props
    Статический рендеринг✅ Не блокирует статический рендеринг⚠️ Требует setRequestLocale(); каталоги с пространствами имён по-прежнему исключали страницы из статического рендеринга в наших тестах
    Tree-shaking (отправка только используемого контента)✅ По компоненту, по локали, автоматизировано компилятором⚠️ Ручная работа: пространства имён + pick(messages, [...]) на страницу
    Lazy loadingimportMode: 'dynamic' (одна строка конфигурации)⚠️ Ручной динамический импорт в getRequestConfig
    Purge unused content✅ Dead dictionaries are dropped at build time❌ Not built-in
    Testing missing translations (CLI / CI)npx intlayer content test⚠️ Not built-in; docs suggest npx @lingual/i18n-check
    AI-powered translation✅ Built-in, uses your own provider keys❌ No
    Визуальный редактор / CMS✅ Бесплатный визуальный редактор + опциональная CMS❌ Нет (внешние платформы локализации)
    MCP server & Agent Skills✅ Да❌ Нет
    Экосистема / сообщество⚠️ Меньше, но быстро растет✅ Большое, справочный стандарт Next.js

    Бенчмарк

    Что было измерено

    Benchmark Bloom проводит тесты одного и того же приложения с каждой библиотекой: 10 страниц (home, about, blog, careers, contact, FAQ, pricing, products, settings, team), 10 локалей (en, fr, es, de, it, pt, zh, ja, ko, ru), идентичные компоненты и идентичное содержимое. Страницы измеряются на en и fr. Каждая библиотека реализуется до четырех стратегий загрузки, от наивной установки до оптимальной:

    СтратегияОписаниеКто это делает
    staticВсе локали и все страницы объединены вместеБыстрые прототипы, AI-generated код
    dynamicЗагружается только активная локаль, но все страницы сразуБольшинство проектов
    scoped-staticПространства имён по маршрутам, без ленивой загрузкиРедкий случай
    scoped-dynamicПространства имён по маршрутам + ленивая загрузка. Отправляется только текущая страница в текущей локалиПриложения с строгим бюджетом производительности

    Intlayer не имеет варианта "scoped": компилятор автоматически распределяет контент по компонентам, поэтому его строки static и dynamic уже распределены по области видимости.

    Для каждой сборки набор тестов записывает:

    • Lib size: gzip размер пустого компонента, который только импортирует i18n библиотеку. Фиксированная стоимость runtime.
    • Page JS: gzip JavaScript, скачанный на страницу, усреднённый по всем страницам и локалям.
    • Locale leak %: доля переведённых строк, найденных в скачанном JS, которые принадлежат локали, которую пользователь не просматривает (отпечатано на en и fr, поэтому 50% означает "другая измеренная локаль полностью присутствует"; с 10 связанными локалями, реальные потери выше).
    • Page leak %: доля переведённых строк, найденных в скачанном JS, которые принадлежат странице, на которой пользователь не находится.
    • Component avg: средний gzip размер каждого компонента, скомпилированного в изоляции. Показывает, сколько i18n runtime один компонент тянет за собой.
    • E2E reactivity: время в реальном масштабе времени между выбором нового языка и обновлением html[lang] в DOM (Playwright, 5 итераций).
    • Hydration: продолжительность фазы гидрации React.
    Приведённые ниже числа получены из запуска от 2026-09-12 с next-intl 4.14.2, use-intl 4.14.2 и intlayer 9.5.1. Тестовое приложение намеренно небольшое (несколько десятков строк на язык), поэтому процентные значения утечек описывают паттерн: они растут вместе с вашим контентом, тогда как стоимость runtime остаётся постоянной.

    Результаты на Next.js (App Router)

    LibraryStrategyLib size (gz)Page JS avg (gz)Locale leakPage leakComponent avg (gz)E2E reactivityHydration
    base (no i18n)-0.0 KB141.0 KB0.0%0.0%0.9 KB13.4 ms11.8 ms
    next-intlstatic14.7 KB153.6 KB4.2%89.8%21.8 KB16.0 ms14.7 ms
    next-intldynamic14.7 KB153.6 KB9.7%89.9%21.8 KB15.6 ms14.8 ms
    next-intlscoped-static14.7 KB153.6 KB0.0%0.0%80.1 KB17.9 ms17.4 ms
    next-intlscoped-dynamic14.7 KB153.6 KB0.0%0.0%22.9 KB17.8 ms16.8 ms
    next-intlayerstatic5.5 KB141.3 KB0.0%0.0%8.5 KB15.5 ms16.9 ms
    next-intlayerdynamic5.5 KB141.3 KB0.0%0.0%6.9 KB15.3 ms15.9 ms
    @intlayer/next-intl (compat)static8.0 KB147.5 KB0.0%0.0%8.1 KB14.5 ms12.8 ms
    @intlayer/next-intl (compat)dynamic8.0 KB148.7 KB0.0%0.0%8.1 KB11.7 ms12.8 ms

    Как это читать

    • Стоимость runtime. Базовое приложение весит 141.0 KB на страницу. next-intl доводит это значение до 153.6 KB (+12.6 KB gzip на каждой странице), Intlayer до 141.3 KB (+0.3 KB). Этот разрыв не зависит от количества строк: это runtime библиотеки.
    • Leakage. В двух конфигурациях, которые большинство команд фактически используют (static и dynamic), next-intl доставляет ~90% строк иностранных страниц на каждую страницу: весь en.json попадает в клиентский провайдер. Чтобы достичь 0%, требуются конфигурации scoped-*: разделить каталоги на namespaces, затем выбрать нужные с помощью pick() на каждой странице. Intlayer находится на 0% в обоих случаях без всего этого.
    • JavaScript на странице не изменился для next-intl между стратегиями. Тестовое содержимое небольшое, поэтому утечка ~90% составляет всего несколько KB здесь. В реальном приложении со сотнями строк на странице это соотношение становится доминирующей стоимостью. Тем временем +12.6 KB runtime оплачивается в каждой конфигурации.
    • Размер компонента. Компонент, который вызывает useTranslations(), компилируется в среднем до 21.8 KB; тот же компонент с useIntlayer() компилируется до 6.9 KB. В конфигурации scoped-static компоненты next-intl увеличиваются до 80.1 KB, потому что каждый встраивает свой каталог пространства имен.
    • Реактивность и гидратация находятся в одном диапазоне для обеих библиотек на Next.js (15-18 мс). Ни одна из них не является узким местом здесь.

    Результаты на TanStack Start (use-intl)

    use-intl - это не зависящее от фреймворка ядро next-intl. Тот же API, тот же формат сообщений. Его сравнение с intlayer на TanStack Start исключает из уравнения части, специфичные для Next.js.

    БиблиотекаСтратегияРазмер библиотеки (gz)Среднее JS страницы (gz)Утечка локалиУтечка страницыСреднее размер компонента (gz)E2E реактивность
    base (без i18n)-0.0 KB111.0 KB0.0%0.0%0.7 KB8.1 ms
    use-intlstatic14.1 KB179.8 KB50.0%89.8%76.0 KB6.7 ms
    use-intldynamic14.1 KB119.4 KB0.0%89.8%75.9 KB7.0 ms
    use-intlscoped-static14.1 KB128.7 KB0.0%0.0%87.1 KB20.9 ms
    use-intlscoped-dynamic14.1 KB128.7 KB0.0%0.0%87.1 KB13.3 ms
    intlayerstatic5.0 KB125.8 KB50.0%0.0%8.1 KB3.2 ms
    intlayerdynamic5.0 KB118.6 KB0.0%0.0%6.3 KB3.6 ms
    @intlayer/use-intl (compat)dynamic7.3 KB129.7 KB0.0%0.0%9.3 KB8.7 ms

    Как это читать

    • Наивная конфигурация use-intl доставляет на 68.8 KB больше JS на страницу чем базовое приложение, причём половина строк принадлежит неправильной локали и 90% неправильной странице.
    • use-intl в режиме dynamic достигает 119.4 KB, близко к 118.6 KB Intlayer, но все еще несет 89.8% утечку страницы: строки всех страниц для активной локали загружаются на каждой странице. Их группировка по маршруту (scoped-*) устраняет утечку, но добавляет еще ~9 KB накладных расходов на chunk.
    • В Intlayer строка static уже имеет 0% утечку страницы: компилятор собирает только словари, используемые компонентами на странице. Включение importMode: 'dynamic' (одна строка в intlayer.config.ts) также устраняет утечку локали.
    • Размер компонента показывает разницу в архитектуре: 76-87 KB на компонент с use-intl против 6-8 KB с Intlayer. useTranslations() привязывает каждый компонент к глобальному дереву сообщений; useIntlayer() привязывает его к собственному словарю.
    • Переключение локали в 2-4 раза быстрее с Intlayer (3 мс против 7-21 мс).

    Почему такая разница? Централизованные каталоги vs скомпилированные словари

    next-intl следует классической модели: один JSON на локаль, загруженный в getRequestConfig, переданный в NextIntlClientProvider, доступный через t("namespace.key").

    bash
    .
    ├── messages
       ├── en.json
       └── fr.json
    └── src
        ├── i18n
       ├── request.ts
       └── routing.ts
        ├── middleware.ts
        └── app
            └── [locale]
                ├── layout.tsx
                └── about
                    └── page.tsx
    

    Runtime не может знать, какие ключи будет использовать страница, поэтому безопасный стандарт - отправить весь каталог. Оптимизация означает, что вы разделяете каталог на пространства имён, вы решаете, какие пространства имён нужны каждой странице, и вы поддерживаете это соответствие в синхронизации при перемещении компонентов. Строка scoped-dynamic в тесте производительности - это награда за эту работу, и большинство команд туда никогда не доходят.

    Intlayer переворачивает ответственность. Контент объявляется рядом с компонентом:

    bash
    .
    ├── intlayer.config.ts
    └── src
        ├── middleware.ts
        ├── app
       └── [locale]
           ├── layout.tsx
           └── about
               ├── page.tsx
               └── page.content.ts
        └── components
            └── Counter
                ├── index.tsx
                └── index.content.ts
    

    При сборке компилятор (@intlayer/swc / @intlayer/babel) видит, какой компонент импортирует какой словарь. Он объединяет только те словари, только для активной локали, и удаляет те, которые ничто не импортирует. Паттерн "scoped-dynamic" становится результатом сборки вместо дисциплины, которую команда должна поддерживать.

    Чтобы получить цифры dynamic строки, установите dictionary.importMode: 'dynamic' в intlayer.config.ts. См. документацию по оптимизации bundle.

    Опыт разработчика

    Client компонент

    next-intl

    messages/en.json
    {
      "counter": {
        "label": "Counter",
        "increment": "Increment"
      }
    }
    
    src/components/Counter.tsx
    "use client";
    
    import { useState } from "react";
    import { useTranslations, useFormatter } from "next-intl";
    
    export const Counter = () => {
      const t = useTranslations("counter");
      const format = useFormatter();
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>{format.number(count)}</p>
          <button aria-label={t("label")} onClick={() => setCount((c) => c + 1)}>
            {t("increment")}
          </button>
        </div>
      );
    };
    
    Помните, что нужно включить пространство имён counter в сообщения, передаваемые в NextIntlClientProvider на каждой странице, которая отображает этот компонент.

    Intlayer

    src/components/Counter/index.content.ts
    import { t, type Dictionary } from "intlayer";
    
    const counterContent = {
      key: "counter",
      content: {
        label: t({ ru: "Счётчик", en: "Counter", fr: "Compteur" }),
        increment: t({ ru: "Увеличить", en: "Increment", fr: "Incrémenter" }),
      },
    } satisfies Dictionary;
    
    export default counterContent;
    
    src/components/Counter/index.tsx
    "use client";
    
    import { useState } from "react";
    import { useIntlayer } from "next-intlayer";
    import { useNumber } from "next-intlayer/format";
    
    export const Counter = () => {
      const { label, increment } = useIntlayer("counter");
      const number = useNumber();
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>{number(count)}</p>
          <button aria-label={label} onClick={() => setCount((c) => c + 1)}>
            {increment}
          </button>
        </div>
      );
    };
    

    Не требуется регистрировать на странице: компонент содержит свой собственный контент.

    Синхронный серверный компонент

    Компоненты системы проектирования (navbar, footer, карточки) часто являются серверными компонентами, отрендеренными как дочерние элементы клиентских компонентов, поэтому они не могут быть async.

    next-intl

    src/components/ServerCounter.tsx
    type ServerCounterProps = {
      t: (key: string) => string;
      formattedCount: string;
    };
    
    export const ServerCounter = ({ t, formattedCount }: ServerCounterProps) => (
      <div>
        <p>{formattedCount}</p>
        <button aria-label={t("label")}>{t("increment")}</button>
      </div>
    );
    

    Страница должна await getTranslations("counter") и await getFormatter(), затем передать результаты вниз как props. Компонент больше не является самостоятельным.

    Intlayer

    src/components/ServerCounter.tsx
    import { useIntlayer } from "next-intlayer/server";
    import { useNumber } from "next-intlayer/server/format";
    
    export const ServerCounter = ({ count }: { count: number }) => {
      const { label, increment } = useIntlayer("counter");
      const number = useNumber();
    
      return (
        <div>
          <p>{number(count)}</p>
          <button aria-label={label}>{increment}</button>
        </div>
      );
    };
    

    Метаданные

    next-intl

    src/app/[locale]/about/page.tsx
    import type { Metadata } from "next";
    import { getTranslations } from "next-intl/server";
    import { routing } from "@/i18n/routing";
    
    // Функция для получения локализованного пути
    const localizedPath = (locale: string, path: string) =>
      locale === routing.defaultLocale ? path : `/${locale}${path}`;
    
    // Генерация метаданных для страницы
    export const generateMetadata = async ({
      params,
    }: {
      params: Promise<{ locale: string }>;
    }): Promise<Metadata> => {
      const { locale } = await params;
      const t = await getTranslations({ locale, namespace: "about" });
    
      const languages = Object.fromEntries(
        routing.locales.map((l) => [l, localizedPath(l, "/about")])
      );
    
      return {
        title: t("title"),
        description: t("description"),
        alternates: {
          canonical: localizedPath(locale, "/about"),
          languages: { ...languages, "x-default": "/about" },
        },
      };
    };
    

    Intlayer

    src/app/[locale]/about/page.tsx
    import { getIntlayer, getMultilingualUrls } from "intlayer";
    import type { Metadata } from "next";
    import type { LocalPromiseParams } from "next-intlayer";
    
    export const generateMetadata = async ({
      params,
    }: LocalPromiseParams): Promise<Metadata> => {
      const { locale } = await params;
      const metadata = getIntlayer("about-metadata", locale);
      const multilingualUrls = getMultilingualUrls("/about");
    
      return {
        ...metadata,
        alternates: {
          canonical: multilingualUrls[locale as keyof typeof multilingualUrls],
          languages: { ...multilingualUrls, "x-default": "/about" },
        },
      };
    };
    

    Сохраните API next-intl, получите вывод Intlayer

    Вам не нужно переписывать компоненты, чтобы получить указанные выше числа производительности. @intlayer/next-intl - это готовый адаптер: он сохраняет useTranslations, getTranslations, useFormatter, t.rich(), ICU plurals и помощники next-intl/navigation, и предоставляет их из словарей Intlayer, скомпилированных компилятором Intlayer.

    next.config.ts
    import type { NextConfig } from "next";
    import { createNextIntlPlugin } from "@intlayer/next-intl/plugin";
    
    const withIntlayer = createNextIntlPlugin();
    
    const nextConfig: NextConfig = {};
    
    export default withIntlayer(nextConfig);
    

    В benchmark, compat-сборка того же приложения сократилась с 153.6 KB до 147.5 KB за страницу, с 21.8 KB до 8.1 KB за компонент, и с ~90% page leakage на 0%, при этом код приложения остался без изменений. Ваши существующие файлы messages/{locale}.json могут остаться источником истины благодаря JSON sync plugin.

    Смотрите руководство миграции next-intl для пошагового процесса.

    Когда выбрать что?

    • Выбирайте next-intl, если вам нужен стандарт экосистемы для Next.js, вы полагаетесь на ICU MessageFormat, ваше приложение небольшого или среднего размера, или вы интегрируетесь с платформой перевода (Crowdin, Phrase, Lokalise...), которая ожидает централизованный JSON. Учтите время на организацию пространств имён каталогов и выбор сообщений для каждой страницы, если производительность важна.
    • Выбирайте Intlayer, если вам нужно контент, привязанный к компонентам, строгий TypeScript, ошибки missing-key на этапе сборки, автоматический tree-shaking и ленивая загрузка, синхронные серверные компоненты и встроенные инструменты редактирования (Visual Editor, CMS, перевод с помощью AI, MCP server). Особенно актуально для больших модульных кодовых баз и дизайн-систем.
    • Выбирайте @intlayer/next-intl, если вы уже используете next-intl и хотите сэкономить на размере бандла без полного переписывания.

    Связанные сравнения

    GitHub STARs

    GitHub stars - это сильный индикатор популярности проекта, доверия сообщества и долгосрочной актуальности. Хотя это не прямая мера технического качества, они отражают, сколько разработчиков находят проект полезным, следят за его развитием и, вероятно, готовы его использовать.

    Star History Chart

    Заключение

    next-intl - это надежная, хорошо поддерживаемая библиотека, и benchmark подтверждает, что это далеко не худший вариант для Next.js. Однако её централизованная модель каталога возлагает все оптимизации на разработчика: наивная конфигурация пропускает ~90% контента иностранных страниц, а само runtime стоит +12,6 KB gzip на каждой странице.

    Intlayer переносит эту работу в компилятор. Словари для каждого компонента, ленивая загрузка для каждого локала и очистка мертвого контента являются выходными данными сборки, а не соглашениями. Результат в том же приложении: +0.3 KB на страницу, 0% утечек, компоненты в 3 раза меньше и переключение локали в 2-4 раза быстрее на TanStack Start.

    Все необработанные данные, тестовые приложения и скрипты находятся в репозитории Benchmark Bloom. Запустите его самостоятельно.

    Обратитесь к документации 'Why Intlayer?' для получения дополнительных сведений.

    Комментарии

    Пока нет комментариев. Будьте первым, кто поделится своими мыслями.

    Похожие сообщения

    Последние сообщения