作者:
    Creation:2026-09-13Last update:2026-09-22

    Lingui VS Intlayer | React & Next.js 国际化 (i18n) 基准测试对比

    JavaScript i18n library ecosystem

    Lingui 和 Intlayer 是本次基准测试中仅有的两个依赖编译器而非纯运行时的国际化库。Lingui 在构建时从宏中提取消息,并为每个语言环境编译目录。Intlayer 按组件编译字典,并按语言环境进行 Tree-shaking。理论上它们的表现应当十分接近,但实际测试数据揭示了它们在哪方面走向了分歧。

    测试数据来源于 Benchmark Bloom,这是一个开源基准测试套件,它使用每个库构建完全相同的应用程序,并记录浏览器实际下载和执行的代码细节。

    摘要 (tl;dr):在每页原始 JavaScript 下载体积方面,Lingui 最接近 Intlayer:在配置了延迟加载(lazy loading)后,TanStack Start 上为 115-120 KB 对 118.6 KB,Next.js 上为 148.6 KB 对 141.3 KB。但差距体现在其他维度:独立编译的 Lingui 组件体积高达 58-153 KB,而 Intlayer 仅为 6-8 KB;水合耗时 Lingui 需要 28-34 ms,而 Intlayer 仅需 11-14 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 消息格式支持⚠️ 正在演进中✅ 原生支持(plural、select、selectOrdinal 等宏)
    格式化处理(日期、数字、货币)✅ useNumber、useDate 等(底层基于 Intl)✅ i18n.date()、i18n.number()
    本地化路由与中间件✅ 内置代理/中间件,提供 getMultilingualUrls❌ 非核心功能,无内置方案
    SEO 辅助工具(hreflang、sitemap、robots)✅ 内置全套开箱即用工具❌ 需手动实现
    同步服务端组件(RSC)✅ next-intlayer/server 的 useIntlayer 支持在任意子服务端组件中同步使用⚠️ 每个请求都需要一个 I18n 实例,需通过 Props 层层传递或使用 setI18n 设置
    Tree-shaking(仅打包使用内容)✅ 按组件、按语言环境,由编译器完全自动化处理⚠️ 通过 lingui compile 实现按语言切分;按路由切分需手动拆解目录
    延迟加载(Lazy loading)✅ importMode: 'dynamic'(仅需一行配置)⚠️ 需手动 import() 编译后的目录并调用 i18n.load() / i18n.activate()
    清理未使用内容✅ 废弃字典在构建时自动丢弃✅ lingui extract --clean 可剔除过时消息
    CI / 命令行缺失翻译测试✅ npx intlayer content test⚠️ lingui extract 提供统计(默认不返回非零退出码)
    构建流水线✅ 单一插件即可(@intlayer/swc / @intlayer/babel / vite-intlayer)⚠️ 需宏插件(Babel 或 SWC)加上独立的 extract 和 compile 步骤
    AI 自动化翻译✅ 内置集成,使用自备 API Key(OpenAI、Anthropic、Mistral 等)❌ 无
    可视化编辑器 / CMS✅ 提供免费 Visual Editor + 可选云端 CMS❌ 无(.po 文件依赖外部 TMS 平台)
    MCP 服务端与 Agent Skills✅ 支持❌ 无
    生态系统与社区成熟度⚠️ 较年轻但发展极其迅猛✅ 历经长期检验,框架无关

    性能基准测试

    评测标准与设置

    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 生成代码
    dynamic仅通过 import() 载入当前活动语言的目录,但包含所有页面内容大多数常规项目
    scoped-static按路由切分目录,但在初始化时全部打包进首屏极少使用
    scoped-dynamic按路由拆解目录 + 延迟 import()。仅下载当前页面及当前语言的内容对流量和体积有严格预算的应用

    Intlayer 无需单独的 "scoped" 变体:编译器自动按组件维度界定内容范围,因此其 static 与 dynamic 行本身就已经实现了按需切分。

    每项构建测试均记录以下核心指标:

    • Lib size:仅导入 i18n 库的空组件的 gzip 体积(固定运行时成本)。
    • Page JS:每页下载的 gzip JavaScript 平均体积(在所有页面和语言中取均值)。
    • Locale leak %:下载的 JS 中,属于用户当前未查看语言环境的翻译文本比例。
    • Page leak %:下载的 JS 中,属于用户当前未访问页面的翻译文本比例。
    • Component avg:每个组件单独编译时的平均 gzip 体积。
    • E2E reactivity:从选择新语言到 DOM 中 html[lang] 属性完成更新的挂钟耗时(Playwright,取 5 次测试均值)。
    • Hydration:React 水合阶段的执行耗时。
    以下数据来自 2026-09-12 的评测记录,测试版本为 @lingui/react 6.6.0 与 intlayer 9.5.1。测试应用规模精简(每种语言几十个字符串),因此泄漏比例反映的是一种结构性规律:随着项目内容增长,泄漏量将成正比放大。

    Next.js 测试结果

    选择您关注的指标和库:

    动态 JSON 加载

    在运行时懒加载翻译

    有作用域的 JSON (命名空间)

    每页翻译命名空间

    I18n 性能基准测试

    这个指标是什么?

    国际化库包的总 gzip 压缩大小。它仅包含 tree-shaking 和压缩(minification)后的提供者(provider)和内容检索逻辑。

    为什么这很重要?

    较小的库大小可减少初始 JavaScript 负载,从而缩短客户端的下载和执行时间。

    视图形式

    库策略库体积 (gz)页面 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 代码的 Base 应用仅增加 0.3 KB。
    • 初级配置开销惊人。 一次性加载所有语言目录会导致每页达到 207.4 KB,比基础应用剧增 66 KB。其中一半翻译属于非目标语言,90% 属于其他页面。
    • 动态加载仅解决了语言切分,未解决页面级冗余。 单一语言全量目录方案中,页面泄漏率仍徘徊在 90% 左右:无论访问哪个子页面,整个法语目录都会全量下发。Lingui 要达到 0% 页面泄漏,必须采用 scoped-dynamic(手动按路由抽取、编译并在页面单独挑选)。
    • 源语言回退导致的固有泄漏。 即便在最佳配置下,仍有 3-15% 的英文原文字符串被打包到法语页面中。这是因为 Lingui 宏保留了原文字符串作为运行时安全回退。Intlayer 在构建期便彻底解决了回退关系,客户端仅传输目标语言。
    • scoped-dynamic 下组件体积急剧放大。 隔离编译下的单个组件平均体积暴增至 152.6 KB,因为通过导入引用,该组件将所有路由目录全都牵连了进来。而在 Intlayer 中,同样的组件使用 useIntlayer() 平均仅有 6.9 KB。
    完整表格、所有库和策略请参阅 Next.js 基准测试报告。

    TanStack Start 测试结果

    库策略库体积 (gz)页面 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。其经过哈希处理的编译目录极其紧凑,配合 TanStack Start 出色的路由拆分能力,使得其在 dynamic 阶段就能达到 0% 页面泄漏。
    • 但在其他各项体验指标上,Intlayer 全面胜出。 水合耗时 Lingui 需要 28-34 ms,而 Intlayer 仅需 11-14 ms:Lingui 的 i18n.load() + i18n.activate() 必须在客户端优先于 React 水合完成。单组件隔离体积 Lingui 为 58-148 KB,而 Intlayer 为 6-8 KB。由于回退机制,Lingui 的语言泄漏率始终无法归零。
    • 优化配置下切换语言存在卡顿。 scoped-dynamic 下的 Lingui 耗时 42 ms 才能更新 html[lang],因为新的路由目录必须经历网络请求、加载与激活才能反映在视图上。Intlayer 在两种模式下均稳定在 3-4 ms 极速完成。
    • Intlayer 的 static 模式已天然具备 0% 页面泄漏,因为打包器只打包当前页面所渲染组件显式导入的字典。仅需增加一行配置(importMode: 'dynamic')即可同时消除语言泄漏。
    • @intlayer/lingui 允许开发者继续沿用 Lingui 宏语法,底层由 Intlayer 字典直接服务。它牺牲了少许整页体积(由于宏运行时驻留,为 137 KB),换取了大幅缩小的单组件(12.8 KB)和显著加快的水合速度。对于既有项目而言是非常理想的平滑升级通道。
    完整表格请参阅 TanStack Start 基准测试报告。

    根本成因剖析:两个编译器,两种不同的工作单元

    The Intlayer compiler extracts content from components

    两款库都引入了编译阶段。核心差异在于它们究竟在编译什么。

    Lingui 编译的是目录(Catalogs)。 源代码中的宏被提取到单语言的 .po 文件中,继而被编译为单语言的 JS 模块。它的工作单元是语言环境整体(Locale)。若想进一步细分(按路由或组件),就必须创建多个目录,并在 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 编译的是字典(Dictionaries)。 每个 .content.ts 文件都是一个绑定到特定键的独立字典。编译器自动分析哪个组件导入了哪个键,并在字典及语言两个维度精确产出该组件所必需的 JSON。它的工作单元是组件(Component)。路由级隔离只是自然产生的结果:一个页面只会请求所渲染组件的字典。

    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 中则是一项复杂的工程配置项目. 差距在页面和语言两个维度上同时拉大:

    Theoretical content leakage by architecture

    若要复现 dynamic 行的性能指标,只需在 intlayer.config.ts 中声明 dictionary.importMode: 'dynamic'。详见 打包优化文档。

    开发者体验对比 (DX)

    基础初始化

    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.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.ts 中加入 intlayer()(在 Next.js 中使用 withIntlayer()),再用 <IntlayerProvider> 包裹组件树。无需独立的提取或编译命令行操作:启动打包器时字典自动完成编译。

    组件内编写

    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 的哈希键下。如果开发者遗漏了提取或编译步骤,界面将静默显示英文原文。

    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 的错误提示。

    组件树之外的环境

    如路由元数据、加载器(loaders)、服务端执行函数等非 React 树环境。

    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,手动导入正确的目录,并必须采用 msg + i18n._() 而不能直接写 t。正如 基准测试记录 所指出的,判断何时该使用 t、 t` ` 、i18n.t()、msg 还是 <Trans>,非常不够直观。

    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()],
    });
    

    构建流水线中保留 @lingui/babel-plugin-lingui-macro / @lingui/swc-plugin,确保其在 Intlayer 编译器之前执行。参阅 Lingui 兼容适配器文档。

    该如何做出选型抉择?

    如果您想要带有类型化宏的 ICU MessageFormat,您的翻译人员在已有 TMS 流程中使用 .po 文件,您喜欢在 JSX 中就近书写源语言字符串,并且您的团队乐于掌控提取 / 编译 / 目录拆分的工作流。配置好懒加载后,其每页 JS 体积非常有竞争力。

    如果您想要组件级作用域内容、严格的 TypeScript、构建期缺失键报错、零成本 tree-shaking 与懒加载、微小的组件体积、快速注水、即时语言切换以及内置编辑工具(可视化编辑器、CMS、AI 翻译、MCP 服务器)。特别适用于大型、模块化代码库与设计系统。

    如果您已在使用 Lingui,并希望在无需修改宏代码的情况下渐进式迁移到 Intlayer 字典。您的 .po 目录通过 PO 同步插件 仍然保持为唯一事实来源。在 Lingui vs @intlayer/lingui 中并排测试。

    常见问题

    因为编译的单元不同。Lingui 按语言编译单一目录:其之下的细分(按路由分包、懒加载、将 fallback 排除在 bundle 之外)全部需要繁琐配置。Intlayer 按组件编译字典,因此路由级作用域自然由构建器生成。这就是为什么单独编译一个 Lingui 组件体积高达 58-153 KB,而 Intlayer 仅为 6-8 KB。

    宏会将源码中的原始消息保留为运行时兜底(fallback),因此英文文本会紧随翻译内容一同打包。基准测试在所有优化方案中均测得 fr 页面中含有 3-15% 的 en 文本。Intlayer 在构建期解析 fallback,并且只打包当前激活的语言。

    是的,在 TanStack Start 上它甚至微弱胜出:dynamic 模式下为 115.2 KB(Intlayer 为 118.6 KB)。采用哈希 ID 的编译目录非常紧凑。但开销体现在其他方面:注水耗时 28-34 ms(Intlayer 为 11-14 ms),在 scoped-dynamic 配置下语言切换耗时高达 42 ms。

    不需要。@intlayer/lingui 让 t`...` 、<Trans>、msg、plural、select 和 selectOrdinal 保持原样编译;仅仅是 i18n._() 底层解析的数据来源发生了改变。在构建中保留 @lingui/babel-plugin-lingui-macro 或 @lingui/swc-plugin 即可。参见 Lingui 兼容性文档。

    宏代码继续保留这两个步骤,但对于 Intlayer 自身的内容则完全不需要。.content.ts 字典在打包器运行时自动生成,无需单独的 CLI 命令,并且 intlayer test 会在缺失键时直接让 CI 报错,而不是静默回退到源文本。

    相关对比评测

    相同基准测试,其他库:

    深入了解:

    参考文档:

    GitHub 星标发展历程

    GitHub Star 是直观体现项目受欢迎程度、社区信赖度以及长期活力的风向标。尽管星标数不直接等同于技术优劣,但它生动反映了有多少开发者认可并乐于在项目中采纳该方案。

    星标历史趋势图

    总结

    Lingui 毫无疑问是本次评测中最为强劲的“运行时+编译器”混合型库。其高度精简且哈希化的编译目录,使其单页 JavaScript 体积紧追 Intlayer,在 TanStack Start 上甚至实现了微弱的反超。如果仅仅把每页纯代码大小当成唯一指标,两者几乎可以打成平手。

    但综合体验并非仅此一项。Lingui 的编译局限在语言环境这一级;其下的所有性能优化(按路由细分、延迟加载、剔除回退字符串)全部转嫁给了开发者的手动工程配置。基准测试清晰展现了这种边界所付出的代价:组件体积放大 10-20 倍、水合变慢 2-3 倍、3-15% 无法根除的语言泄漏,以及优化模式下 42 ms 的语言切换停顿。而 Intlayer 的编译器在组件粒度上展开工作,使得这些指标无需任何繁琐设置就能原生达到 6-8 KB、11-14 ms、0% 与 3-4 ms 的极致体验。

    所有的原始测试数据、测试应用与执行脚本均已在 Benchmark Bloom 代码仓库 中完整开源。欢迎亲自克隆并运行验证。

    欲了解更多设计哲学,请参阅 “为什么选择 Intlayer?”文档。

    评论

    暂无评论。成为第一个分享您想法的人吧。

    相关文章

    最新文章