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

    Lingui VS Intlayer | React & Next.js 国際化 (i18n) ベンチマーク比較

    LinguiとIntlayerは、本ベンチマークにおいて純粋なランタイムではなくコンパイラを活用する2つのライブラリです。Linguiはビルド時にマクロからメッセージを抽出し、言語ごとにカタログをコンパイルします。Intlayerはコンポーネント単位で辞書をコンパイルし、言語ごとにTree-shakingを行います。理論上は非常に近い性能になるはずですが、実測データはその決定的な違いを浮き彫りにしています。

    測定データは、各ライブラリで同一のアプリケーションを構築しブラウザが実際にダウンロード・実行する内容を記録するオープンソースの検証スイート Benchmark Bloom に基づいています。

    要約 (tl;dr): ページ単体の純粋なJavaScript容量において、LinguiはIntlayerに最も肉薄しています。遅延ロード(lazy loading)設定後のTanStack Startでは 115〜120 KB118.6 KB、Next.jsでは 148.6 KB141.3 KB です。しかし、それ以外の指標で大きな差が生じます。個別にコンパイルされたコンポーネントサイズはIntlayerの 6〜8 KB に対しLinguiは 58〜153 KB、ハイドレーション所要時間はIntlayerの 11〜14 ms に対し 28〜34 ms、最適化構成であっても英語フォールバック文字列がフランス語ページに 3〜15% リークし、その最適化構成を組むにはルートごとにカタログを手動で抽出・コンパイル・選択する必要があります。Intlayerはこれらをゼロ構成で達成します。

    概要比較

    • Lingui - マクロベース( t`...` <Trans>msg)、ICU MessageFormat構文、.po / JSONカタログ、lingui extract + lingui compile ワークフロー。メッセージIDをハッシュに圧縮し、言語ごとの動的カタログロードに対応。長年の実績がありフレームワーク非依存、.po を軸とした翻訳ツールとの連携が強力。
    • Intlayer - コンポーネント中心のコンテンツモデル。各コンポーネントの隣に .content.ts 辞書を配置。ビルド時コンパイラがコンポーネント・言語単位でTree-shakingと遅延ロードを自動実行。コンテンツから厳密なTypeScript型を生成し、未翻訳キーをビルドエラーとして検出。ミドルウェア、SEOヘルパー、ビジュアルエディタ / CMS、AI翻訳機能を標準搭載。
    ライブラリGitHub Stars総コミット数最終コミット初版公開NPMバージョンNPMダウンロード数
    aymericzip/intlayerGitHub Repo starsGitHub commit activityLast Commit2024年4月npmnpm downloads
    lingui/js-linguiGitHub Repo starsGitHub commit activityLast Commit2016年12月npmnpm downloads
    バッジは自動更新されます。スナップショットは時間の経過とともに変化します。

    機能の直接比較

    機能Intlayer (react-intlayer / next-intlayer)Lingui (@lingui/core / @lingui/react)
    コンポーネント近傍での翻訳管理✅ 対応、各コンポーネントと同階層に .content.ts を配置⚠️ JSX内にマクロで原文をインライン記述、翻訳は一括 .po カタログで管理
    TypeScript統合✅ コンテンツから厳格な型定義を自動生成⚠️ マクロ自体は型付きだがメッセージIDは型付けされず、カタログの不足を検出不可
    未翻訳テキストの検出✅ TypeScriptエラー + ビルド時の警告/エラー検出⚠️ lingui extract で統計表示、実行時は英語原文へフォールバック
    リッチコンテンツ(JSX / Markdown / コンポーネント)✅ ネイティブ対応✅ ネストされたコンポーネントを持つ <Trans>
    ICU構文サポート⚠️ 開発中✅ 対応(pluralselectselectOrdinal マクロ)
    フォーマット処理(日付、数値、通貨)useNumber, useDate, ...(内部で Intl を使用)i18n.date(), i18n.number()
    ローカライズルーティング & ミドルウェア✅ 組み込みプロキシ/ミドルウェア、getMultilingualUrls❌ コア機能としては非対応
    SEOヘルパー(hreflang、sitemap、robots)✅ 組み込みヘルパー提供❌ 手動実装が必要
    同期Server Components(RSC)next-intlayer/serveruseIntlayer が任意の子サーバーコンポーネントで動作⚠️ リクエストごとに I18n インスタンスが必要(Propsバケツリレーまたは setI18n
    Tree-shaking(使用コンテンツのみ配信)✅ コンポーネント単位・言語単位でコンパイラが全自動処理⚠️ lingui compile で言語別、ルート別は手動でのカタログ分割が必要
    遅延ロード(Lazy loading)importMode: 'dynamic'(設定1行のみ)⚠️ コンパイル済みカタログの import() + i18n.load() / i18n.activate() を手動実装
    未使用コンテンツのパージ✅ 不要な辞書はビルド時に自動削除lingui extract --clean で不要メッセージを削除可能
    未翻訳テスト(CLI / CI)npx intlayer content test⚠️ lingui extract の統計出力(デフォルトではエラー終了コードを返さない)
    ビルドパイプライン✅ プラグイン1つ(@intlayer/swc / @intlayer/babel / vite-intlayer⚠️ マクロプラグイン(BabelまたはSWC) + extract + compile の個別ステップ
    AI翻訳機能✅ 組み込み対応(OpenAI、Anthropic、Mistralなど自身のAPIキーを使用)❌ なし
    ビジュアルエディタ / CMS✅ 無料ビジュアルエディタ + オプションのCMS❌ なし(.po 形式による外部TMSとの連携)
    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)、同一のコンポーネント構成および同一のコンテンツ。測定は enfr のページで行われます。各ライブラリは最大4つのロード戦略で評価されました:

    戦略説明主な想定用途
    static全言語のコンパイル済みカタログをはじめに一括インポート・ロードクイックプロトタイプ、AI生成コード
    dynamicimport() で表示言語のカタログのみ取得するが、全ページ分が含まれる一般的な大半のプロジェクト
    scoped-staticルートごとに1カタログを分割し、最初にすべてバンドルまれなケース
    scoped-dynamicルートごとにカタログ分割 + 遅延 import()。現在のページ・言語のみを配信厳格なパフォーマンス予算を持つアプリ

    Intlayerには個別の "scoped" バリアントは存在しません。コンパイラが自動的にコンポーネント単位でコンテンツを分割するため、static および dynamic 行がすでに最適化された状態となっています。

    各ビルドにおいて以下のメトリクスを記録しています:

    • Lib size: i18nライブラリのみをインポートする空コンポーネントのgzipサイズ(ランタイム固定コスト)。
    • Page JS: 全ページおよび全言語の平均として、1ページあたりにダウンロードされるJavaScriptのgzipサイズ。
    • Locale leak %: ダウンロードされたJSのうち、ユーザーが閲覧していない言語に属する文字列の割合。
    • Page leak %: ダウンロードされたJSのうち、ユーザーが滞在していないページに属する文字列の割合。
    • Component avg: 各コンポーネントを単独でコンパイルしたときの平均gzipサイズ。
    • E2E reactivity: 言語選択からDOM内の html[lang] が更新されるまでの実測時間(Playwright、5回試行の平均)。
    • Hydration: Reactハイドレーションフェーズの所要時間。
    下記の数値は、@lingui/react 6.6.0 および intlayer 9.5.1 を使用した 2026-09-12 の実行結果です。テストアプリは意図的に軽量(言語あたり数十文字列程度)に設定されているため、リーク率はコンテンツ量に応じて拡大する構造的パターンを表しています。

    Next.jsでの測定結果

    ライブラリ戦略Lib size (gz)Page JS 平均 (gz)言語リークページリークコンポーネント平均 (gz)E2E応答性ハイドレーション
    base (i18nなし)-0.0 KB141.0 KB0.0%0.0%0.9 KB13.4 ms11.8 ms
    Linguistatic11.9 KB207.4 KB50.0%90.0%73.3 KB15.3 ms15.2 ms
    Linguidynamic11.9 KB145.4 KB2.8%89.9%19.9 KB15.7 ms12.7 ms
    Linguiscoped-static11.9 KB148.2 KB2.7%89.1%20.4 KB15.1 ms13.1 ms
    Linguiscoped-dynamic11.9 KB148.6 KB14.8%0.0%152.6 KB16.1 ms14.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

    データの読み解き方

    • ランタイムコストの差。 空のコンポーネントはLinguiで11.9 KB gzip、Intlayerで5.5 KBです。ページ全体では、Linguiの最適構成はIntlayerに対し +7.3 KB(148.6 KB 対 141.3 KB)であり、Intlayerはi18n未導入のベースアプリに対してわずか +0.3 KB の増加にとどまります。
    • 初歩的な全カタログ一括ロードは極めて重い。 全言語のカタログを最初にロードする設定では 1ページあたり207.4 KB となり、ベースアプリから+66 KBも肥大化します。文字列の半分は不要な言語であり、90%は開いていないページの内容です。
    • 動的ロードは言語を解決してもページを解決しない。 言語ごとに1カタログにまとめた場合、フランス語の全ページ用カタログが毎回送信されるためページリークは90%前後に留まります。Linguiでページリークを0%にするには scoped-dynamic(ルートごとにカタログを分割・抽出し、ページごとに手動読み込みする設計)が必要です。
    • 原文フォールバックによるリーク。 最適化構成であっても、フランス語ページに英語文字列が3〜15%混入します。Linguiマクロはフォールバック用として原文をバンドル内に保持するためです。Intlayerはビルド時にフォールバックを解決し、要求された言語のみを配信します。
    • scoped-dynamic でコンポーネントが肥大化。 個別にコンパイルされたコンポーネントは平均 152.6 KB に達します。インポートを通じて各ルートのカタログに到達可能となるためです。同じコンポーネントを useIntlayer() で組んだ場合は平均 6.9 KB です。

    TanStack Startでの測定結果

    ライブラリ戦略Lib size (gz)Page JS 平均 (gz)言語リークページリークコンポーネント平均 (gz)E2E応答性ハイドレーション
    base (i18nなし)-0.0 KB111.0 KB0.0%0.0%0.7 KB8.1 ms21.6 ms
    Linguistatic11.2 KB152.2 KB50.0%90.0%58.0 KB3.9 ms19.9 ms
    Linguidynamic11.2 KB115.2 KB9.3%0.0%85.5 KB5.9 ms28.0 ms
    Linguiscoped-static11.2 KB120.8 KB4.0%0.0%147.9 KB7.1 ms33.9 ms
    Linguiscoped-dynamic11.2 KB120.2 KB8.6%0.0%83.7 KB42.1 ms32.9 ms
    intlayerstatic5.0 KB125.8 KB50.0%0.0%8.1 KB3.2 ms11.5 ms
    intlayerdynamic5.0 KB118.6 KB0.0%0.0%6.3 KB3.6 ms14.1 ms
    @intlayer/lingui (互換)dynamic10.3 KB137.0 KB9.9%0.0%12.8 KB2.9 ms19.7 ms

    データの読み解き方

    • ページあたりのJS量ではLinguiが僅差で優勢。 dynamic 設定のLinguiは 115.2 KB であり、Intlayerの118.6 KBを3.4 KB下回ります。ハッシュ化IDを持つLinguiのコンパイル済みカタログは非常に高密度であり、TanStack Startのルーター分割が優秀なため dynamic の段階でページリークが0%になります。
    • それ以外のほぼすべての指標でIntlayerがリード。 ハイドレーション時間はLinguiの 28〜34 ms に対しIntlayerは 11〜14 ms です(i18n.load() + i18n.activate() がReactのハイドレーション前に実行されるため)。個別コンポーネントサイズもLinguiの 58〜148 KB に対しIntlayerは 6〜8 KB、言語リークもフォールバック仕様によりLinguiは0%になりません。
    • 最適化構成での言語切り替えに遅延が発生。 scoped-dynamic のLinguiは、ルートカタログのフェッチ・ロード・アクティベーションを待つため html[lang] の更新に 42 ms かかります。Intlayerは両モードとも 3〜4 ms で即座に切り替わります。
    • Intlayerの static は最初からページリーク0%。 該当ページのコンポーネントがインポートする辞書のみをバンドルするためです。設定に importMode: 'dynamic' を1行追加すれば言語リークも完全に解消されます。
    • @intlayer/lingui はLinguiのマクロ構文を維持したままIntlayerの辞書からコンテンツを供給します。ページサイズ(マクロランタイムが残るため137 KB)を少し犠牲にする代わりに、コンポーネントの軽量化(12.8 KB)とハイドレーションの高速化を実現します。既存プロジェクトからの優れた移行手段となります。

    なぜ差がつくのか? 2つのコンパイラと2つの作業単位

    両者ともコンパイルを行います。決定的な違いは何をコンパイルするかです。

    Linguiはカタログをコンパイルします。 ソース内のマクロは言語ごとの .po ファイルに抽出され、言語ごとのJSモジュールにコンパイルされます。単位はロケール(言語全体)です。これ以上細かく(ルート別やコンポーネント別に)分割するには、複数のカタログを作成し、lingui.config.ts でファイルごとに抽出元を設定し、各ルートで適切なファイルを読み込む必要があります。I18n インスタンスはグローバルであり、すべての useLingui() がコンポーネントをそこへ接続します。

    bash
    .
    ├── lingui.config.ts
    └── src
        ├── i18n.ts                          # setupI18n(), load(), activate()
        ├── locales
       ├── en
       ├── messages.po
       └── messages.mjs             # lingui compileの出力
       └── fr
           ├── messages.po
           └── messages.mjs
        ├── components
       └── Counter.tsx                  # const { t } = useLingui(); t`Increment`
        └── routes
            └── $locale
                └── about.tsx                # await import(`../locales/${locale}/messages.mjs`)
    

    Intlayerは辞書をコンパイルします。.content.ts ファイルはキーに紐づいた辞書です。コンパイラはどのコンポーネントがどのキーをインポートしているかを解決し、辞書・言語ごとにコンポーネントが必要とする最小限のJSONを出力します。単位はコンポーネントです。ルート別のスコープ化はその自然な結果であり、ページは自身が表示するコンポーネントの辞書しか読み込みません。

    bash
    .
    ├── intlayer.config.ts
    └── src
        ├── components
       └── Counter
           ├── index.tsx                # useIntlayer("counter")
           └── index.content.ts
        └── routes
            └── $locale
                ├── about.tsx
                └── about.content.ts
    

    これが、scoped-dynamic パターンがIntlayerにとって「自動出力」であり、Linguiにとっては「手動の設定プロジェクト」となる理由です。

    dynamic 行の測定結果を再現するには、intlayer.config.tsdictionary.importMode: 'dynamic' を指定してください。詳細は バンドル最適化ドキュメント をご覧ください。

    開発者体験(DX)

    初期設定

    Lingui

    lingui.config.ts
    import { defineConfig } from "@lingui/cli";
    
    export default defineConfig({
      sourceLocale: "en",
      locales: ["en", "fr"],
      catalogs: [
        {
          path: "<rootDir>/src/locales/{locale}/messages",
          include: ["src"],
        },
      ],
    });
    
    src/i18n.ts
    import { setupI18n } from "@lingui/core";
    
    export const loadCatalog = async (locale: string) => {
      const { messages } = await import(`./locales/${locale}/messages.mjs`);
      const i18n = setupI18n();
      i18n.load(locale, messages);
      i18n.activate(locale);
      return i18n;
    };
    

    さらに @lingui/babel-plugin-lingui-macro(または @lingui/swc-plugin)をバンドラーに追加し、コード編集後に lingui extract を実行し、ビルド前に lingui compile を行い、ツリーを <I18nProvider i18n={i18n}> でラップします。

    Intlayer

    intlayer.config.ts
    import { type IntlayerConfig, Locales } from "intlayer";
    
    const config: IntlayerConfig = {
      internationalization: {
        locales: [Locales.ENGLISH, Locales.FRENCH],
        defaultLocale: Locales.ENGLISH,
      },
    };
    
    export default config;
    

    vite.config.tsintlayer() を追加(Next.jsの場合は next.config.tswithIntlayer() を追加)し、ツリーを <IntlayerProvider> でラップするだけです。抽出やコンパイルの個別コマンドは不要で、バンドラー実行時に自動処理されます。

    コンポーネント実装

    Lingui

    src/components/Counter.tsx
    import { useState } from "react";
    import { useLingui } from "@lingui/react/macro";
    import { Trans } from "@lingui/react/macro";
    
    export const Counter = () => {
      const { t, i18n } = useLingui();
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>{i18n.number(count)}</p>
          <button aria-label={t`Counter`} onClick={() => setCount((c) => c + 1)}>
            <Trans>Increment</Trans>
          </button>
        </div>
      );
    };
    

    英語テキストはコンポーネント内に記述し、フランス語テキストは lingui extract 実行後に src/locales/fr/messages.po 内のハッシュ化IDの下に生成されます。抽出やコンパイルを忘れると、無言で英語原文が表示されます。

    Intlayer

    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
    import { useState } from "react";
    import { useIntlayer } from "react-intlayer";
    import { useNumber } from "react-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>
      );
    };
    

    両言語がコンポーネントの隣の同一ファイルに同居します。fr の値が欠けていればビルドエラーになり、キー名を誤記すればTypeScriptが即座にエラーを報告します。

    コンポーネント外部での利用

    メタデータ、ローダー、サーバー関数など、Reactツリーが存在しない場所での利用です。

    Lingui

    src/routes/$locale/about.tsx
    import { setupI18n } from "@lingui/core";
    import { msg } from "@lingui/core/macro";
    
    const title = msg`About us`;
    
    export const loader = async ({ params }: { params: { locale: string } }) => {
      const { messages } = await import(
        `../../locales/${params.locale}/messages.mjs`
      );
      const i18n = setupI18n({
        locale: params.locale,
        messages: { [params.locale]: messages },
      });
    
      return { title: i18n._(title) };
    };
    

    呼び出しごとに新しい I18n インスタンスを生成し、適切なカタログを手動でインポートし、t ではなく msg + i18n._() を使用します。ベンチマークノートでも指摘されている通り、t t` ` i18n.t()msg<Trans> の使い分けは直感的とは言えません。

    Intlayer

    src/routes/$locale/about.tsx
    import { getIntlayer } from "intlayer";
    
    export const loader = async ({ params }: { params: { locale: string } }) => {
      const { title } = getIntlayer("about-metadata", params.locale);
    
      return { title };
    };
    

    Linguiマクロを維持したままIntlayer辞書を利用する

    @intlayer/lingui は、@lingui/core および @lingui/react 用のドロップインアダプターです。マクロはこれまで通りコンパイルされ、内部の i18n._() 呼び出しはIntlayer辞書から提供されます。.po 同期プラグインにより既存のカタログファイルをそのまま真実のソースとして保持可能です。ICUの複数形や条件分岐も同一にレンダリングされます。

    vite.config.ts
    import { defineConfig } from "vite";
    import { lingui } from "@intlayer/lingui/plugin";
    
    export default defineConfig({
      plugins: [lingui()],
    });
    

    ビルド設定において、Intlayerコンパイラの前に @lingui/babel-plugin-lingui-macro / @lingui/swc-plugin を実行するようにしてください。詳細は Lingui互換性ドキュメント をご覧ください。

    どちらを選択すべきか?

    • Linguiを選ぶべきケース: 型付きマクロで ICU MessageFormat をフル活用したい場合、翻訳者が既存のTMS連携のもと .po ファイルで作業している場合、JSX内に原文を直接書きたい場合、および抽出・コンパイル・カタログ分割の工程をチームで無理なく運用できる場合。遅延ロードが確立されていれば、ページ容量は極めて良好です。
    • Intlayerを選ぶべきケース: コンポーネントスコープのコンテンツ厳格なTypeScript型チェックビルド時の未翻訳キー検出設定不要の自動Tree-shaking&遅延ロード、軽量なコンポーネント、高速なハイドレーション、瞬時の言語切り替え、組み込みの編集ツール(Visual Editor、CMS、AI翻訳、MCPサーバー)を求める場合。特に大規模アプリやデザインシステムに最適です。
    • @intlayer/lingui を選ぶべきケース: 既存のLinguiアプリで、マクロを修正することなくIntlayerの辞書アーキテクチャへ段階的に移行したい場合。

    関連する比較記事

    GitHubスターの推移

    GitHubスターは、プロジェクトの人気、コミュニティの信頼、持続可能性を示す重要な指標です。コードの技術的品質を直接測るものではありませんが、どれだけ多くの開発者が有用と認め動向を追っているかを如実に表しています。

    スター履歴チャート

    結論

    Linguiは、本ベンチマークにおいて最も堅牢なランタイム+コンパイラ型ライブラリです。ハッシュ化されたコンパイル済みカタログにより、1ページあたりのJavaScript容量はIntlayerに肉薄し、TanStack Startではわずかに下回るほどです。もしページサイズのみが評価軸であれば、実質的な引き分けと言えます。

    しかし実際はそうではありません。Linguiのコンパイラは言語単位で止まるため、それ以下の最適化(ルート別カタログ、遅延ロード、フォールバック文字列の除外)はすべて手作業の設定頼みとなります。ベンチマークはその代償として、10〜20倍重いコンポーネント2〜3倍遅いハイドレーション解消できない3〜15%の言語リーク、最適化構成における 42 ms の言語切り替え遅延を明確に示しています。Intlayerのコンパイラはコンポーネント単位で動作するため、追加設定なしで 6〜8 KB11〜14 ms0%3〜4 ms を標準で達成します。

    すべての生データ、テストアプリ、スクリプトは Benchmark Bloom リポジトリ で公開されています。ぜひご自身でお確かめください。

    詳細については 「Intlayerを選ぶ理由」ドキュメント をご覧ください。

    コメント

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

    関連記事

    最新の投稿