著者:
    作成:2026-09-13最終更新:2026-09-13

    next-intl VS @intlayer/next-intl | 同じAPI、異なるBundle

    @intlayer/next-intl は互換性アダプタです。next-intl API (useTranslationsgetTranslationsuseLocalet.rich()、ICU複数形、NextIntlClientProvider...)を公開し、Intlayerによってコンパイルされたディクショナリから提供します。アプリケーションコードは変わりません。bundleが変わります。

    この記事は、同じNext.jsアプリケーションで2つを比較しており、1回はnext-intlで構築し、もう1回はアダプタで構築しています。数値はBenchmark Bloomから得られており、これはブラウザが実際にダウンロードするものを記録するオープンソーススイートです。ライブラリとしてnext-intlとIntlayerの比較が必要な場合は、next-intl vs Intlayerをお読みください。このドキュメントは、コンポーネントをそのままにしておいたときにアダプタが何を変更するかについてです。

    tl;dr: 同じNext.jsアプリで、next-intl@intlayer/next-intlに置き換えることで、ページあたりのJavaScriptが153.6 KBから147.5 KB gzipに、平均コンポーネントが21.8 KBから8.1 KBに、外部ページの文字列漏洩が約90%から0%に、ハイドレーションが14.7 msから12.8 msに短縮され、コンポーネント編集なしで達成されました。TanStack Startでは、use-intlの同等物(@intlayer/use-intl)がコンポーネントを76-87 KBから9-11 KBに削減し、ロケール切り替えを7-21 msから4-9 msに短縮しました。アダプターのランタイムコストは8.0 KBnext-intl14.7 KB、ネイティブnext-intlayer5.5 KB)です。ナビゲーションとミドルウェアはIntlayerのルーティング設定で再実装されます。ローカライズされたpathnamesは唯一引き継がれない機能です。

    @intlayer/next-intlとは何か

    next-intl はランタイムです: getRequestConfig はリクエストごとに messages/{locale}.json を読み込み、NextIntlClientProvider がそれをクライアントに送り、useTranslations("about") がレンダリング時にそのオブジェクトからキーを読み込みます。すべての最適化(名前空間、ページごとの pick(messages, [...])、遅延読み込み)はあなたが書く必要があります。

    @intlayer/next-intl はそのチェーンの最初と最後の部分を保つ代わりに、中間部分を置き換えます。あなたのコンポーネントは依然として useTranslations("about") を呼び出しますが、受け取る内容はビルド時にコンパイルされた Intlayer 辞書から来ており、そのコンポーネントにスコープされ、アクティブなロケールのみが含まれます。

    3 つのメカニズムでこれを実現します:

    1. Import aliasing. createNextIntlPlugin() from @intlayer/next-intl/pluginwithIntlayer をラップし、Webpack / Turbopack aliases を追加します。これにより、next-intlnext-intl/servernext-intl/navigationnext-intl/middleware@intlayer/next-intl に解決されます。codebase 内のいかなる import も変更されません。
    2. JSON as source of truth. syncJSON plugin は既存の messages/{locale}.json を読み取り、その top-level keys を namespace ごとに 1 つの dictionary に分割し、CLI または CMS がそれらを更新する際に同じファイルに翻訳を書き込みます。translator のワークフローは変わりません。
    3. Call-site binding。 Intlayer optimize pass (Babel または SWC) は useTranslations("about") を、about dictionary を直接受け取る呼び出しに書き換えます。コンポーネントはもはやグローバルメッセージツリーにアクセスせず、自身のコンテンツにアクセスします。
    app/[locale]/about/page.tsx
    // あなたのコード、変更なし
    import { useTranslations } from "next-intl";
    
    const AboutPage = () => {
      const t = useTranslations("about");
      return <h1>{t("title")}</h1>;
    };
    
    What the compiler emits (simplified)
    // コンパイラが出力するもの (簡略版)
    import _dicHash_about from "../.intlayer/dictionaries/about.mjs";
    import { useDictionary as useTranslations } from "@intlayer/next-intl";
    
    const AboutPage = () => {
      const t = useTranslations(_dicHash_about);
      return <h1>{t("title")}</h1>;
    };
    

    そのリライトが、以下のコンポーネントサイズとページリーケージのカラムが移動する理由です:ページは、それがレンダリングするコンポーネントの辞書のみを取得し、提供されているロケールでのみ取得します。

    アダプターが保持、無視、および置き換えないもの

    next-intl API@intlayer/next-intlを使用している場合
    useTranslations("ns") / getTranslations("ns")✅ 保持。ビルド時にns辞書にバインドされます。キーはコンテンツに対して型付けされます。
    getTranslations({ locale, namespace })✅ 保持されています
    t("key", { name }), t.rich(), t.markup(), t.raw()✅ 保持されています。ICU plurals、selectselectordinal#{ts, date, long} は Intlayer の ICU resolver を通じて実行されます
    useLocale() / getLocale() / setRequestLocale() / setLocale✅ 保持されています
    useFormatter()✅ 保持されています。dateTimenumberrelativeTimelistdateTimeRange はネイティブな Intl へ橋渡しされます
    NextIntlClientProvider✅ 維持。messagestimeZonenow プロップは受け入れられていますが無視されます(開発者向け警告が表示されます)
    getMessages()✅ 互換性のため維持;もう必要ありません
    getRequestConfig() in src/i18n.ts⚠️ 不要。辞書はビルド時にコンパイルされます;リクエストごとのメッセージ読み込みはありません
    defineRouting()✅ 維持。省略されたフィールド(localesdefaultLocalelocalePrefix)は intlayer.config.ts から読み込まれます
    createNavigation(), Link, redirect, usePathname, useRouter✅ 保持。Intlayer のルーティング設定で再実装されます。routing 引数は受け入れられていますが無視されます
    pathnames (ローカライズされたルート名)❌ 型指定用に受け入れられます。補間されません。プレーンパス名を保つか、そのマッピングを Intlayer の rewrite に移動してください
    createMiddleware()✅ 保持。Intlayer のプロキシを返します。NEXT_LOCALE cookie を設定するので、useLocale() とあなたのスイッチャーは機能し続けます
    NEXT_LOCALE cookie✅ デフォルトで読み込まれます(routing.storage を自分で設定しない限り)
    Bare useTranslations() with no namespace⚠️ 動作しますが、呼び出しサイトがバインドされていません: ランタイムレジストリを通じて解決されます。バンドルの利得を得るには namespace を渡してください

    ベンチマーク

    測定内容

    Benchmark Bloom スイートは、各セットアップで同じアプリケーションをビルドします: 10 ページ (home、about、blog、careers、contact、FAQ、pricing、products、settings、team)、10 locales (enfresdeitptzhjakoru)、同一のコンポーネントと同一のコンテンツ。ページは enfr で測定されます。

    next-intlは4つのローディング戦略で構築されました。単純なセットアップ(messages/{locale}.json全体を読み込む)から最適なもの(ルートごとに1つのnamespace + ページごとのpick())まで。アダプターは単純なセットアップと同じコンポーネントで構築され、next.config.tsintlayer.config.tsのみが変更されました。「scoped」バリアントはありません。コンパイラがコンポーネントごとにコンテンツをスコープするため、staticdynamicの行はすでにスコープされています。

    各ビルドについて、スイートは以下を記録します:

    • Lib size: i18nライブラリのみをインポートする空のコンポーネントのgzipサイズ。ランタイムの固定コスト。
    • Page JS: ページごとにダウンロードされるgzip JavaScript。すべてのページとロケールで平均化されます。
    • ロケールリーク %: ダウンロードされた JS に含まれる翻訳済み文字列のうち、ユーザーが表示していないロケールに属する文字列の割合。
    • ページリーク %: ダウンロードされた JS に含まれる翻訳済み文字列のうち、ユーザーが閲覧していないページに属する文字列の割合。
    • Component avg: 個別にコンパイルされた各コンポーネントの平均 gzip サイズ。単一のコンポーネントが i18n ランタイムとカタログにどの程度のオーバーヘッドをもたらすかを示します。
    • E2E reactivity: 新しいロケールを選択してから DOM の html[lang] が更新されるまでの実際の時間 (Playwright、5 回のイテレーション)。
    • Hydration: React ハイドレーション フェーズの継続時間。
    以下の数値は 2026-09-12 に実施した実行結果です。next-intl / use-intl 4.14.2 および @intlayer/* 9.5.1 を使用しています。テストアプリケーションは意図的に小規模(ロケールあたり数十の文字列)であるため、リーケージのパーセンテージはパターンを説明しています。コンテンツが増えるにつれてリーケージは増加しますが、runtime のコストは固定のままです。

    Next.js での結果

    SetupStrategyLib 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
    @intlayer/next-intlstatic8.0 KB147.5 KB0.0%0.0%8.1 KB14.5 ms12.8 ms
    @intlayer/next-intldynamic8.0 KB148.7 KB0.0%0.0%8.1 KB11.7 ms12.8 ms
    next-intlayer (native)static5.5 KB141.3 KB0.0%0.0%8.5 KB15.5 ms16.9 ms
    next-intlayer (native)dynamic5.5 KB141.3 KB0.0%0.0%6.9 KB15.3 ms15.9 ms

    読み方

    • 同じコンポーネント、ページあたり 6 KB 削減。 アダプター ビルドのナイーブなアプリは 147.5 KB に到達し、完全に最適化されたもの (153.6 KB) を含む すべての next-intl 構成の下にあります。ランタイム自体が違い:8.0 KB 対 14.7 KB で、すべてのページで支払われます。
    • リークは何も変更することなく 0% に到達します。 ナイーブな next-intl セットアップは、すべてのページで外国ページ文字列の約 90% を配信しています。next-intl で 0% に到達するには、scoped-* セットアップが必要です:ルートごとに 1 つの namespace、および各ページで pick(messages, [...]) を使用します。アダプターは、optimize パスが各 useTranslations("ns") をそれ独自の辞書にバインドするため、ナイーブなコードから 0% に到達します。
    • コンポーネントは 2.7 倍縮小されます。 分離されたコンパイルされたコンポーネントは、next-intl の場合は平均 21.8 KB(プロバイダーとメッセージツリーに達します)で、アダプターの場合は 8.1 KB です。next-intlscoped-static セットアップでは、その数は 80 KB に上がります。なぜなら、すべてのルートの namespace ファイルがそれを選択するページから到達可能になるからです。
    • Hydrationが2ms高速 (12.8 vs 14.7 ms): RSCペイロードから逆シリアル化するメッセージオブジェクトがないため、Reactが水和できます。
    • アダプターはネイティブランタイムではありません。 next-intlayer141.3 KBで、ベースアプリの上に+0.3 KBで、5.5 KBのランタイムを備えています。アダプターはIntlayerのコア上にnext-intl APIサーフェス(useFormattert.rich、ICUリゾルバ)を搭載しており、したがって8.0 KBと1ページあたり+6 KBです。これはブリッジであり、目的地ではありません。

    TanStack Start上の結果 (use-intl)

    use-intlnext-intlのフレームワークに依存しないコアです。そのアダプター@intlayer/use-intlは、Viteプラグイン(@intlayer/use-intl/plugin)を使用して同じ設計に従います。

    セットアップストラテジーLib サイズ (gz)ページ JS 平均 (gz)ロケール漏洩ページ漏洩コンポーネント平均 (gz)E2E レスポンシビティハイドレーション
    base (i18n なし)-0.0 KB111.0 KB0.0%0.0%0.7 KB8.1 ms21.6 ms
    use-intlstatic14.1 KB179.8 KB50.0%89.8%76.0 KB6.7 ms15.3 ms
    use-intldynamic14.1 KB119.4 KB0.0%89.8%75.9 KB7.0 ms15.4 ms
    use-intlscoped-static14.1 KB128.7 KB0.0%0.0%87.1 KB20.9 ms24.8 ms
    use-intlscoped-dynamic14.1 KB128.7 KB0.0%0.0%87.1 KB13.3 ms25.9 ms
    @intlayer/use-intlstatic7.3 KB135.8 KB49.7%0.0%10.9 KB4.2 ms10.5 ms
    @intlayer/use-intldynamic7.3 KB129.7 KB0.0%0.0%9.3 KB8.7 ms16.1 ms
    intlayer (native)static5.0 KB125.8 KB50.0%0.0%8.1 KB3.2 ms11.5 ms
    intlayer (native)dynamic5.0 KB118.6 KB0.0%0.0%6.3 KB3.6 ms14.1 ms

    読み方

    • ページあたりのバイト数は最適化された use-intl と同等です。 @intlayer/use-intldynamic モード (129.7 KB) は use-intlscoped-dynamic (128.7 KB) と 1 KB の差以内であり、use-intl の通常の dynamic (119.4 KB) より 10 KB 上回っています。その通常の dynamic 行は外部ページの文字列の 90% を依然リークしています。バイト数が低いのはテストアプリのコンテンツが小さいためです。アダプターの 0% はコンテンツが増加しても一定に保たれるものです。
    • コンポーネントは7~9倍小さい。 use-intlコンポーネントは平均76~87 KBでこれはすべての戦略で同じです。これはuseTranslationsがプロバイダーの全メッセージオブジェクトにバインドされているためです。アダプターは平均9~11 KBです。
    • ロケール切り替えが高速。 最適化されたuse-intlセットアップはhtml[lang]を更新するのに13~21 msかかります。アダプターは4~9 msかかります。再レンダリングされるコンポーネントが少なく、メッセージツリーから再取得されるものがありません。
    • staticはすべてのロケールを保持する。 アダプターのstatic行は49.7%のロケールリークを示しており、これはネイティブIntlayerのstaticモードと同じです。すべてのロケールがバンドルされ、ページの辞書のみが対象です。1行の設定(importMode: 'dynamic')でこれを削除できます。

    数字が変わる理由

    コンポーネント内では何も変更されていないため、利益はすべてuseTranslationsがバインドされているもの由来です。

    next-intlの場合、バインディングはプロバイダーです。NextIntlClientProviderはロケールのmessagesオブジェクト全体を受け取り、すべてのuseTranslations("about")がそこから読み込まれます。バンドラーは1つのコンポーネントが1つのフックをインポートしており、そのフックが1つのコンテキストを読み込んでいることを認識しますが、aboutブランチのみが使用されていることを知ることはできません。以下のルートはすべて同じメッセージオブジェクトを共有しているため、page-leakカラムは自分でファイルを分割するまで〜90%を読み込みます。

    bash
    .
    ├── messages
       ├── en.json                       # あらゆるnamespace、あらゆるpage
       └── fr.json
    └── src
        ├── i18n.ts                       # getRequestConfig({ messages: await import(...) })
        ├── middleware.ts                 # createMiddleware(routing)
        └── app/[locale]
            ├── layout.tsx                # <NextIntlClientProvider messages={messages}>
            └── about/page.tsx            # useTranslations("about")
    

    @intlayer/next-intlを使用する場合、バインディングは辞書です。syncJSONmessages/en.jsonを1つのトップレベルキーごとに1つの辞書に変換します。コンパイラはuseTranslations("about")を呼び出すコンポーネントを解決し、アクティブなロケールでaboutを直接渡します。これはbundlerが追跡して分割できるimportです。

    bash
    .
    ├── intlayer.config.ts                # syncJSON({ source: ({ locale }) => `./messages/${locale}.json` })
    ├── messages
       ├── en.json                       # 変更なし、真実のソースのまま
       └── fr.json
    ├── .intlayer/                        # 生成: 名前空間とロケールごとに1つの辞書
    └── src
        ├── middleware.ts                 # createMiddleware() は現在 Intlayer のプロキシを返します
        └── app/[locale]
            ├── layout.tsx                # <NextIntlClientProvider> (messages prop なし)
            └── about/page.tsx            # useTranslations("about")  ← 変更なし
    

    src/i18n.tsmessages prop は廃止されます。その他はすべて同じです。

    3 つのステップでの移行

    1. インストール

      bash
      npx intlayer init --interactive
      

      このコマンドはnext-intlを検出し、intlayernext-intlayer@intlayer/next-intl@intlayer/sync-json-pluginをインストールします。next-intlをインストール状態のままにしてください。これはアダプターのピア依存関係であり、型を提供しています。

    2. Intlayerをメッセージに指す

      intlayer.config.ts
      import { Locales, type IntlayerConfig } from "intlayer";
      import { syncJSON } from "@intlayer/sync-json-plugin";
      
      const config: IntlayerConfig = {
        internationalization: {
          locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],
          defaultLocale: Locales.ENGLISH,
        },
        dictionary: {
          // "static"はすべてのロケールをバンドルします。"dynamic"はオンデマンドでアクティブなものをロードします
          importMode: "dynamic",
        },
        plugins: [
          syncJSON({
            // ICUプレースホルダー: {name}, {count, plural, one {# item} other {# items}}
            format: "icu",
            source: ({ locale }) => `./messages/${locale}.json`,
            location: "messages",
          }),
        ],
      };
      
      export default config;
      

      messages/{locale}.json はそのままの場所に留まります。各トップレベルのキーは辞書になります。useTranslations("about")about 辞書にマッピングされます。

    3. next.config.ts をラップする

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

      createNextIntlPlugin()withIntlayer (コンテンツ監視、辞書コンパイル、最適化パス) と Webpack および Turbopack の next-intl@intlayer/next-intl エイリアスを構成します。ビルドすれば、上記の表の数字があなたのものになります。

    その後削除できるもの

    ファイル / パターン理由
    src/i18n.tsgetRequestConfigリクエストごとのメッセージ読み込みがありません。ファイルは createNavigation ヘルパーもエクスポートする場合のみ保持してください
    messages={...} on NextIntlClientProviderアダプターはコンパイル済み出力を読み込みます。このプロップは無視され、開発時に警告がログに記録されます
    await getMessages() in layouts同じ理由
    Per-page pick(messages, [...])コンパイラーがコンポーネントごとにピッキングを実行します

    バイト数以上に得られるもの

    • 型付きキー。 useTranslations("about") はコンパイル済み about 辞書に対して型チェックされます。t("does.not.exist") はランタイムのフォールバックではなく TypeScript エラーになります。
    • npx intlayer test はロケールにキーが不足している場合にCIを失敗させます。npx intlayer fill は、選択したプロバイダー(OpenAI、Anthropic、Mistral、Gemini など)を使用して不足しているキーを翻訳し、独自のキーを使用して結果を messages/{locale}.json に書き込みます。
    • Visual Editor と CMS は同じ辞書を操作するため、開発者以外のユーザーは UI を通じて messages/fr.json を編集でき、ファイルが更新されます。
    • .content.ts への段階的な移行。 どのコンポーネントでも、useTranslations("about") から useIntlayer("about") へ、コンポーネントの近くにあるコンテンツファイルとともに、一度に 1 つずつ切り替えることができます。JSON と .content.ts の辞書は共存してマージされます。

    開始する前に知っておくべき制限事項

    • ルーティング設定が intlayer.config.ts に移動します。 createNavigation(routing)createMiddleware(routing) はシグネチャを保持しますが、引数を無視します。ロケール、デフォルトロケール、プレフィックス戦略は Intlayer の routing 設定から取得されます。next-intl のローカライズされた pathnames/about/a-propos)を使用している場合、アダプタはそれらをインターポレートしません。Intlayer の routing.rewrite がそのケースをカバーしますが、これは別の変更です。
    • 名前空間なしの useTranslations() はバインドされません。 最適化パスは、どの辞書をインポートするかを知るための静的な名前空間が必要です。ベアコールは依然として機能しますが、すべての辞書を参照するランタイムレジストリを通じて機能します。これは正確に削除しようとしていたリークです。名前空間を渡してください。
    • アダプターは無料ではありません。 next-intlayer の 5.5 KB に対して 8.0 KB のランタイム、ネイティブビルドよりページあたり +6-7 KB です。これは next-intl の API サーフェスの代償です。すべてのコンポーネントが useIntlayer に移行した時点で、アダプターを削除してください。
    • プロバイダーの messagestimeZonenow は無視されます。 フォーマッターはネイティブの Intl によってバックアップされており、ロケールのみがその出力に影響します。ハイドレーション安定日時のために強制タイムゾーンや固定の now に依存している場合は、呼び出しサイトで処理してください。

    どれを使うべきか?

    • next-intl に留まってください アプリが小規模で、バンドルサイズが問題でなく、チームがネームスペースとページごとの pick() の管理に問題がない場合。
    • @intlayer/next-intlを使用する 現在next-intlを使用していて、バンドルサイズの削減、リークの防止、ハイドレーションの改善、型安全なキー、CLIおよびCMSツールが必要で、書き直したくない場合に推奨されます。これは既存のnext-intl codebaseへの推奨エントリーポイントです。
    • ネイティブに移行する(next-intlayer 新しいプロジェクト、またはアダプターがその役割を終えた後に推奨されます。3つの中で最も軽量(5.5 KB、ページあたり+0.3 KB)で、同期型サーバーコンポーネント、コンポーネント単位の.content.tsファイル、および全機能セットをアンロックします。

    関連する比較

    結論

    @intlayer/next-intl は 1 つのことを行います。useTranslations をバインドする対象を、すべてのメッセージを保持するプロバイダーから、そのコンポーネント用にコンパイルされた dictionary に変更します。1 ページあたり 6 KB の価値がある同じ Next.js アプリで、2.7 倍小さいコンポーネント0% のリーケージ2 ms のハイドレーション が実現でき、誰もコンポーネント ファイルを開く前に達成されます。Navigation と middleware は Intlayer のルーティング設定の上で API を維持し、ネイティブ next-intlayer ランタイムはさらに軽いままです。

    すべてのraw data、テスト アプリ、およびスクリプトは Benchmark Bloom リポジトリ にあります。自分で実行してください。

    詳細については、'Why Intlayer?' ドキュメント を参照してください。

    コメント

    まだコメントはありません。最初のコメントを共有しましょう。

    関連記事

    最新の投稿