Автор:
    Дата створення:2026-09-13Останнє оновлення:2026-09-27

    next-intl проти Intlayer: Бенчмарк інтернаціоналізації (i18n) у React та Next.js

    next-intl VS Intlayer

    next-intl є найпопулярнішою бібліотекою i18n для Next.js. Intlayer, це альтернатива на основі компілятора з областю видимості на рівні компонентів. Обидва рішення локалізують застосунок App Router. Питання полягає в тому, скільки коштує кожне з них після збирання проєкту.

    Ця стаття не є посібником. Це порівняння, підтверджене даними з Benchmark Bloom, відкритого набору бенчмарків, який створює однаковий застосунок із кожною бібліотекою та вимірює те, що браузер фактично завантажує та виконує.

    Коротко (tl;dr): next-intl додає щонайменше +12.6 КБ gzip на кожній сторінці лише через свій runtime і призводить до витоку ~90% рядків з інших сторінок у стандартних конфігураціях (static та dynamic). Щоб усунути цей витік, потрібно вручну розбивати каталоги на простори імен та підключати їх окремо для кожної сторінки. Натомість компілятор Intlayer гарантує 0% витоку, втричі менші компоненти та лише +0.3 КБ понад базовий додаток без будь-яких ручних налаштувань.

    Короткий огляд

    • next-intl - Стандарт спільноти Next.js. Централізовані словники JSON для кожної мови, повна підтримка ICU MessageFormat і глибока інтеграція з обробкою запитів та маршрутизацією Next.js.
    • Intlayer - Модель контенту, орієнтована на компоненти. Файли .content.ts розташовані поруч із компонентами, компілятор часу збирання автоматично виконує tree-shaking та ліниве завантаження для кожного компонента і локалі, а також генерує суворі типи TypeScript.
    БібліотекаЗірки GitHubВсього комітівОстанній комітПерша версіяВерсія NPMЗавантаження NPM
    aymericzip/intlayerGitHub Repo starsGitHub commit activityLast CommitКвітень 2024npmnpm downloads
    amannn/next-intlGitHub Repo starsGitHub commit activityLast CommitБерезень 2021npmnpm downloads
    Значки оновлюються автоматично.

    Порівняння функціональності

    ФункціяIntlayer (react-intlayer / next-intlayer)next-intl (next-intl / use-intl)
    Переклади поруч із компонентами✅ Так, .content.ts поруч із кожним компонентом❌ Централізовані словники JSON у папці messages/
    Інтеграція з TypeScript✅ Суворі типи генеруються автоматично з контенту⚠️ Підтримується через ручне налаштування global.d.ts
    Виявлення відсутніх перекладів✅ Помилка TypeScript + помилка/попередження під час збирання⚠️ У runtime повертає ключ або генерує помилку залежно від налаштувань
    Багатий контент (JSX / Markdown / компоненти)✅ Пряма підтримка⚠️ Через t.rich() з передачею компонентів зіставлення
    Підтримка ICU MessageFormat⚠️ У розробці✅ Так, повна підтримка ICU
    Синхронні серверні компоненти✅ useIntlayer з next-intlayer/server працює у будь-якому дочірньому компоненті❌ Вимагає передачі перекладів через props від асинхронного предка
    Tree-shaking✅ Автоматично для кожного компонента і локалі⚠️ Вимагає ручного поділу на простори імен та використання pick()
    Ліниве завантаження (Lazy loading)✅ Один рядок конфігурації (importMode: 'dynamic')⚠️ Вимагає ручного динамічного імпорту в getRequestConfig
    Візуальний редактор / CMS✅ Безкоштовний Візуальний редактор + додаткова CMS❌ Немає
    Переклад за допомогою ШІ✅ Вбудовано, використовує ваші власні API-ключі❌ Немає
    Сервер MCP та навички агентів✅ Так❌ Немає

    Бенчмарк

    Що вимірювалося

    Набір тестів 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Усі локалі та всі сторінки запаковані разом на стартіШвидкі прототипи, код, згенерований ШІ
    dynamicЗавантажується лише активна локаль, але відразу для всіх сторінокБільшість проєктів
    scoped-staticПростори імен на кожен маршрут, без лінивого завантаженняРідко
    scoped-dynamicПростори імен на кожен маршрут + ліниве завантаження. Лише поточна сторінка поточної мовиДодатки з суворим бюджетом продуктивності

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

    Для кожного збирання фіксувалося:

    • Розмір бібліотеки (Lib size): розмір gzip порожнього компонента, який імпортує лише бібліотеку i18n.
    • JS сторінки (Page JS): кількість стиснутого gzip JavaScript на сторінку.
    • % витоку локалі (Locale leak %): частка рядків мови, яку користувач не переглядає.
    • % витоку сторінки (Page leak %): частка рядків сторінки, на якій користувач не перебуває.
    • Середній розмір компонента (Component avg): середній розмір gzip кожного компонента, скомпільованого окремо.
    • Реактивність E2E: час між перемиканням мови та оновленням html[lang] у DOM.
    • Гідратація: тривалість фази гідратації React.
    Наведені дані отримані під час тестування від 2026-09-12 з next-intl 4.14.2 та intlayer 9.5.1.

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

    Виберіть метрики та бібліотеки, які вас цікавлять:

    Метрика

    Динамічне завантаження JSON

    Ледаче завантаження перекладів під час виконання

    Обмежений JSON (простори імен)

    Простори імен перекладу для кожної сторінки

    Що це за метрика?

    Загальний стиснений у gzip розмір пакета бібліотеки інтернаціоналізації. Він включає лише провайдер та логіку отримання контенту після tree-shaking та мініфікації.

    Чому це важливо?

    Менший розмір бібліотеки зменшує початкове завантаження JavaScript, що призводить до швидшого завантаження та виконання на клієнті.

    Перегляд як

    БібліотекаСтратегіяРозмір Lib (gz)Сер. JS сторінки (gz)Витік локаліВитік сторінкиСер. компонента (gz)Реактивність E2EГідратація
    Базовий додаток (без 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 (сумісн.)static8.0 KB147.5 KB0.0%0.0%8.1 KB14.5 ms12.8 ms
    @intlayer/next-intl (сумісн.)dynamic8.0 KB148.7 KB0.0%0.0%8.1 KB11.7 ms12.8 ms

    Як інтерпретувати результати

    • Витрати runtime. Базовий додаток важить 141.0 КБ на сторінку. next-intl збільшує це значення до 153.6 КБ (+12.6 КБ gzip на кожній сторінці), тоді як Intlayer додає лише 141.3 КБ (+0.3 КБ).
    • Витік контенту. У найпоширеніших конфігураціях (static та dynamic) next-intl відправляє на кожну сторінку ~90% рядків з інших сторінок, оскільки весь файл en.json потрапляє до клієнтського провайдера. Щоб зменшити цей показник до 0%, потрібен складний ручний поділ на простори імен, тоді як в Intlayer це працює за замовчуванням.
    • Розмір компонента. Компонент із useTranslations() важить у середньому 21.8 КБ; той самий компонент з useIntlayer() важить лише 6.9 КБ.
    Повна таблиця, кожна бібліотека та кожна стратегія, у звіті бенчмарку Next.js.

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

    use-intl, це незалежне від фреймворку ядро next-intl. Той самий API, той самий формат повідомлень. Порівняння його з intlayer на TanStack Start усуває особливості Next.js із рівняння.

    БібліотекаСтратегіяРозмір Lib (gz)Сер. JS сторінки (gz)Витік локаліВитік сторінкиСер. компонента (gz)Реактивність E2E
    Базовий додаток (без 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 (сумісн.)dynamic7.3 KB129.7 KB0.0%0.0%9.3 KB8.7 ms

    Як інтерпретувати результати

    • Просте налаштування use-intl передає на 68.8 КБ більше JS на сторінку, ніж базовий додаток.
    • У режимі dynamic use-intl досягає 119.4 КБ, але все одно зберігає 89.8% витоку сторінок.
    • Архітектурна різниця особливо помітна у розмірі компонентів: 76-87 КБ у use-intl проти 6-8 КБ в Intlayer.
    • Перемикання локалі відбувається у 2-4 рази швидше з Intlayer (3 мс проти 7-21 мс).
    Повна таблиця у звіті бенчмарку TanStack Start.

    Чому виникає різниця? Централізовані каталоги проти скомпільованих словників

    Centralized catalogs versus per-component dictionaries

    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 не може передбачити, які саме ключі знадобляться сторінці, тому відправка всього каталогу є єдиним безпечним варіантом.

    Ціна недосягнення цієї мети зростає відразу по двох осях, сторінки та локалі:

    Theoretical content leakage by architecture

    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
    

    Під час збирання компілятор визначає, який компонент імпортує конкретний словник, і пакує тільки ці словники для активної локалі.

    Щоб отримати показники рядка dynamic, встановіть dictionary.importMode: 'dynamic' у intlayer.config.ts. Дивіться документацію з оптимізації бандла.

    Досвід розробника

    Клієнтський компонент

    messages/en.json
    {
      "counter": {
        "label": "Counter",
        "increment": "Increment"
      }
    }
    
    src/components/ClientCounter.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 на кожній сторінці, яка рендерить цей компонент.
    src/components/Counter/index.content.ts
    import { t, type Dictionary } from "intlayer";
    
    const counterContent = {
      key: "counter",
      content: {
        label: t({ en: "Counter", fr: "Compteur" }),
        increment: t({ 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>
      );
    };
    

    На сторінці нічого не потрібно реєструвати: компонент приносить власний вміст.

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

    Елементи дизайн-системи (навігаційні панелі, підвали, картки) часто є серверними компонентами, що відображаються всередині клієнтських компонентів, тому вони не можуть бути async.

    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(), а потім передати результати як пропси. Компонент більше не є автономним.

    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>
      );
    };
    

    Метадані

    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" },
        },
      };
    };
    
    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, обслуговуючи їх зі словників, скомпільованих компілятором 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);
    

    У бенчмарку сумісна збірка того самого додатка зменшила розмір сторінки з 153.6 КБ до 147.5 КБ, розмір компонентів з 21.8 КБ до 8.1 КБ, а витік сторінки знизився з ~90% до 0% без внесення змін у код самого додатка. Ваші наявні файли messages/{locale}.json можуть залишатися основним джерелом даних завдяки плагіну синхронізації JSON.

    Дивіться посібник із міграції з next-intl для отримання покрокових інструкцій.

    Що і коли обрати?

    Вам потрібен стандарт екосистеми для Next.js, ви покладаєтеся на ICU MessageFormat, ваш додаток невеликий або середнього розміру, або ви інтегруєтеся з платформою перекладу (Crowdin, Phrase, Lokalise...), яка очікує централізований JSON. Заплануйте час на поділ каталогів на простори імен і вибір повідомлень через pick() на кожній сторінці, якщо важлива продуктивність.

    Вам потрібен контент з областю видимості компонента, суворий TypeScript, помилки відсутніх ключів на етапі збірки, автоматичний tree-shaking та ліниве завантаження, синхронні серверні компоненти та вбудовані інструменти редагування (Візуальний редактор, CMS, ШІ-переклад, MCP-сервер). Особливо актуально для великих модульних кодових баз та дизайн-систем.

    Ви вже використовуєте next-intl і хочете отримати переваги в розмірі бандла без повного переписування. Адаптер сумісності зберігає ваші імпорти та файл messages/{locale}.json як єдине джерело правди. Порівняно пліч-о-пліч у next-intl проти @intlayer/next-intl.

    Часті запитання

    Не під час рендерингу. Різниця полягає в тому, що надсилається клієнту: next-intl додає +12.6 KB gzip рантайму на кожній сторінці і в стандартних конфігураціях надсилає ~90% рядків чужих сторінок з кожною сторінкою. Перемикання мови та гідратація порівнянні в Next.js (15-18 мс); в TanStack Start use-intl займає 7-21 мс проти 3-4 мс у Intlayer.

    Так, з конфігурацією scoped-dynamic: розділіть messages/{locale}.json на простори імен для кожного маршруту, потім використовуйте pick(messages, [...]) на кожній сторінці і підтримуйте це зіставлення правильним у міру переміщення компонентів. Рядки scoped-* бенчмарку якраз відображають цю роботу. Intlayer досягає 0% за замовчуванням без цього, оскільки компілятор ізолює контент по компонентах. Див. оптимізація бандла.

    Ні. @intlayer/next-intl зберігає useTranslations, getTranslations, useFormatter, t.rich(), множинні форми ICU та помічники навігації, надаючи їх зі скомпільованих словників. Лише один рядок плагіна в next.config.ts. Покроково в посібнику з міграції next-intl.

    Нативна підтримка ICU знаходиться в розробці. Адаптери сумісності (@intlayer/next-intl, @intlayer/use-intl) повністю підтримують ICU: множинні форми, select, selectordinal, # та {ts, date, long} обробляються резолвером ICU від Intlayer. Докладніше читайте в формат повідомлень ICU.

    Так. Плагін синхронізації JSON читає їх, розбиває ключі верхнього рівня на словники та перезаписує переклади в ті самі файли під час оновлення через CLI або CMS. Робочий процес ваших перекладачів не змінюється.

    Схожі порівняння

    Той самий бенчмарк, інші бібліотеки:

    Більше про next-intl:

    Довідкова документація:

    Щоб зрозуміти, звідки взялися ці бібліотеки, прочитайте історію i18n у JavaScript.

    Зірки GitHub

    Зірки на GitHub є важливим показником популярності проєкту, довіри спільноти та його довгострокової актуальності.

    Графік історії зірок

    Активність комітів

    Зірки показують популярність. Коміти показують, скільки роботи вкладено в проєкт. На момент написання Intlayer має близько 7 500 комітів: більше, ніж більшість порівнюваних тут бібліотек, і приблизно в 5 разів більше, ніж next-intl чи next-i18next.

    • amannn/next-intl
    • aymericzip/intlayer

    Коміти в основній гілці, джерело: GitHub API.

    Intlayer це монорепозиторій, тож до підрахунку входять усі пакети для фреймворків, CLI та документація. Сприймайте коміти як показник активності, а не якості.

    Завантаження npm

    • next-intl
    • next-intlayer

    Джерело: API завантажень реєстру npm.

    Кількість завантажень винагороджує найстаріші рішення, а не найкращі. Бібліотеку, випущену багато років тому, досі встановлює кожен проєкт, що обрав її тоді, кожен запуск CI і кожен пакет, який від неї залежить. Ця цифра вимірює інерцію, а не свідомий вибір.

    ШІ-асистенти посилюють цей ефект. next-intl, i18next і vue-i18n трапляються всюди в коді, на якому їх навчали, тож вони пропонують їх за замовчуванням, не порівнюючи альтернатив. Кожна підказка додає завантажень, які живлять наступну підказку. Порівнюйте за бенчмарком, а не за кількістю завантажень.

    Висновок

    next-intl - це надійна, перевірена часом бібліотека, і бенчмарк підтверджує, що вона залишається хорошим вибором для Next.js. Проте централізована модель каталогів перекладає всю оптимізацію на розробника: стандартне налаштування призводить до витоку близько 90% вмісту інших сторінок, а сам runtime коштує +12.6 КБ gzip на кожній сторінці.

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

    Усі вихідні дані, тестові додатки та скрипти доступні у репозиторії Benchmark Bloom.

    Зверніться до документа 'Чому Intlayer?' для отримання детальнішої інформації.

    Коментарі

    Поки що немає коментарів. Будьте першим, хто поділиться своїми думками.

    Схожі публікації

    Останні публікації