@intlayer/docs 8.7.5-canary.0 → 8.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/blog/de/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/blog/en-GB/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/blog/es/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/blog/fr/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/blog/id/list_i18n_technologies/frameworks/svelte.md +0 -2
- package/blog/it/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/blog/ja/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/blog/ko/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/blog/pl/list_i18n_technologies/frameworks/svelte.md +0 -2
- package/blog/pt/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/blog/ru/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/blog/vi/list_i18n_technologies/frameworks/svelte.md +0 -2
- package/blog/zh/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/dist/cjs/generated/docs.entry.cjs +60 -0
- package/dist/cjs/generated/docs.entry.cjs.map +1 -1
- package/dist/esm/generated/docs.entry.mjs +60 -0
- package/dist/esm/generated/docs.entry.mjs.map +1 -1
- package/dist/types/generated/docs.entry.d.ts +3 -0
- package/dist/types/generated/docs.entry.d.ts.map +1 -1
- package/docs/ar/benchmark/index.md +29 -0
- package/docs/ar/benchmark/nextjs.md +227 -0
- package/docs/ar/benchmark/tanstack.md +193 -0
- package/docs/ar/intlayer_with_tanstack.md +0 -2
- package/docs/de/benchmark/index.md +29 -0
- package/docs/de/benchmark/nextjs.md +227 -0
- package/docs/de/benchmark/tanstack.md +193 -0
- package/docs/en/benchmark/___NOTE.md +82 -0
- package/docs/en/benchmark/___nextjs.md +195 -0
- package/docs/en/benchmark/___tanstack.md +187 -0
- package/docs/en/benchmark/index.md +29 -0
- package/docs/en/benchmark/nextjs.md +228 -0
- package/docs/en/benchmark/tanstack.md +217 -0
- package/docs/en-GB/benchmark/index.md +29 -0
- package/docs/en-GB/benchmark/nextjs.md +228 -0
- package/docs/en-GB/benchmark/tanstack.md +193 -0
- package/docs/es/benchmark/index.md +29 -0
- package/docs/es/benchmark/nextjs.md +226 -0
- package/docs/es/benchmark/tanstack.md +193 -0
- package/docs/fr/benchmark/index.md +29 -0
- package/docs/fr/benchmark/nextjs.md +227 -0
- package/docs/fr/benchmark/tanstack.md +193 -0
- package/docs/hi/benchmark/index.md +29 -0
- package/docs/hi/benchmark/nextjs.md +227 -0
- package/docs/hi/benchmark/tanstack.md +193 -0
- package/docs/id/benchmark/index.md +29 -0
- package/docs/id/benchmark/nextjs.md +227 -0
- package/docs/id/benchmark/tanstack.md +193 -0
- package/docs/id/intlayer_with_react_native+expo.md +0 -2
- package/docs/it/benchmark/index.md +29 -0
- package/docs/it/benchmark/nextjs.md +227 -0
- package/docs/it/benchmark/tanstack.md +193 -0
- package/docs/ja/benchmark/index.md +29 -0
- package/docs/ja/benchmark/nextjs.md +227 -0
- package/docs/ja/benchmark/tanstack.md +193 -0
- package/docs/ko/benchmark/index.md +29 -0
- package/docs/ko/benchmark/nextjs.md +227 -0
- package/docs/ko/benchmark/tanstack.md +193 -0
- package/docs/ko/intlayer_with_tanstack.md +0 -2
- package/docs/pl/benchmark/index.md +29 -0
- package/docs/pl/benchmark/nextjs.md +227 -0
- package/docs/pl/benchmark/tanstack.md +193 -0
- package/docs/pt/benchmark/index.md +29 -0
- package/docs/pt/benchmark/nextjs.md +227 -0
- package/docs/pt/benchmark/tanstack.md +193 -0
- package/docs/ru/benchmark/index.md +29 -0
- package/docs/ru/benchmark/nextjs.md +227 -0
- package/docs/ru/benchmark/tanstack.md +193 -0
- package/docs/tr/benchmark/index.md +29 -0
- package/docs/tr/benchmark/nextjs.md +227 -0
- package/docs/tr/benchmark/tanstack.md +193 -0
- package/docs/uk/benchmark/index.md +29 -0
- package/docs/uk/benchmark/nextjs.md +227 -0
- package/docs/uk/benchmark/tanstack.md +193 -0
- package/docs/vi/benchmark/index.md +29 -0
- package/docs/vi/benchmark/nextjs.md +227 -0
- package/docs/vi/benchmark/tanstack.md +193 -0
- package/docs/zh/benchmark/index.md +29 -0
- package/docs/zh/benchmark/nextjs.md +227 -0
- package/docs/zh/benchmark/tanstack.md +193 -0
- package/package.json +6 -6
- package/src/generated/docs.entry.ts +60 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
---
|
|
2
|
+
createdAt: 2026-04-20
|
|
3
|
+
updatedAt: 2026-04-21
|
|
4
|
+
title: 2026 年 Next.js 最佳 i18n 解决方案 - 基准测试报告
|
|
5
|
+
description: 对比 next-intl、next-i18next 和 Intlayer 等 Next.js 国际化 (i18n) 库。关于打包体积、泄漏和响应性的详细性能报告。
|
|
6
|
+
keywords:
|
|
7
|
+
- benchmark
|
|
8
|
+
- i18n
|
|
9
|
+
- intl
|
|
10
|
+
- nextjs
|
|
11
|
+
- 性能
|
|
12
|
+
- intlayer
|
|
13
|
+
slugs:
|
|
14
|
+
- doc
|
|
15
|
+
- benchmark
|
|
16
|
+
- nextjs
|
|
17
|
+
author: Aymeric PINEAU
|
|
18
|
+
applicationTemplate: https://github.com/intlayer-org/benchmark-i18n
|
|
19
|
+
history:
|
|
20
|
+
- version: 8.7.5
|
|
21
|
+
date: 2026-01-06
|
|
22
|
+
changes: "初始化基准测试"
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# Next.js i18n 库 — 2026 年基准测试报告
|
|
26
|
+
|
|
27
|
+
本页面是关于 Next.js i18n 解决方案的基准测试报告。
|
|
28
|
+
|
|
29
|
+
## 目录
|
|
30
|
+
|
|
31
|
+
<Toc/>
|
|
32
|
+
|
|
33
|
+
## 交互式基准测试
|
|
34
|
+
|
|
35
|
+
<I18nBenchmark framework="nextjs" vertical/>
|
|
36
|
+
|
|
37
|
+
## 结果参考:
|
|
38
|
+
|
|
39
|
+
<iframe
|
|
40
|
+
src="https://intlayer.org/markdown?url=https%3A%2F%2Fraw.githubusercontent.com%2Fintlayer-org%2Fbenchmark-i18n%2Fmain%2Freport%2Fscripts%2Fsummarize-nextjs.md"
|
|
41
|
+
width="100%"
|
|
42
|
+
height="600px"
|
|
43
|
+
style="border:none;">
|
|
44
|
+
</iframe>
|
|
45
|
+
|
|
46
|
+
> https://intlayer.org/markdown?url=https%3A%2F%2Fraw.githubusercontent.com%2Fintlayer-org%2Fbenchmark-i18n%2Fmain%2Freport%2Fscripts%2Fsummarize-nextjs.md
|
|
47
|
+
|
|
48
|
+
查看完整的基准测试仓库[点击这里](https://github.com/intlayer-org/benchmark-i18n)。
|
|
49
|
+
|
|
50
|
+
## 简介
|
|
51
|
+
|
|
52
|
+
国际化库对你的应用程序有很大影响。主要的风险在于当用户只访问一个页面时,却加载了所有页面和所有语言的内容。
|
|
53
|
+
|
|
54
|
+
随着应用规模的增长,打包体积会呈指数级增长,这会明显影响性能。
|
|
55
|
+
|
|
56
|
+
例如,在最糟糕的情况下,国际化后的页面体积可能会增加接近 4 倍。
|
|
57
|
+
|
|
58
|
+
i18n 库的另一个影响是开发速度变慢。将组件转换为支持多语言的内容非常耗时。
|
|
59
|
+
|
|
60
|
+
因为这个问题很棘手,所以存在许多解决方案——有些侧重于 DX(开发体验),有些侧重于性能或可扩展性等。
|
|
61
|
+
|
|
62
|
+
Intlayer 尝试在这些维度上进行优化。
|
|
63
|
+
|
|
64
|
+
## 测试你的应用
|
|
65
|
+
|
|
66
|
+
为了发现这些问题,我构建了一个免费扫描器,你可以在[这里](https://intlayer.org/i18n-seo-scanner)试用。
|
|
67
|
+
|
|
68
|
+
<iframe src="https://intlayer.org/i18n-seo-scanner" width="100%" height="600px" style="border:none;"/>
|
|
69
|
+
|
|
70
|
+
## 问题所在
|
|
71
|
+
|
|
72
|
+
有两种主要方法可以限制多语言应用对打包体积的影响:
|
|
73
|
+
|
|
74
|
+
- 将 JSON(或内容)拆分到不同的文件/变量/命名空间中,以便打包工具可以对特定页面未使用的内容进行 Tree-shaking。
|
|
75
|
+
- 仅按用户的语言动态加载页面内容。
|
|
76
|
+
|
|
77
|
+
这些方法的技术限制:
|
|
78
|
+
|
|
79
|
+
**动态加载**
|
|
80
|
+
|
|
81
|
+
即使你使用 Webpack 或 Turbopack 声明了类似 `[locale]/page.tsx` 的路由,并且定义了 `generateStaticParams`,打包工具也不会将 `locale` 视为静态常量。这意味着它可能会将所有语言的内容都拉入每个页面。限制这种情况的主要方法是通过动态导入(例如 `import('./locales/${locale}.json')`)加载内容。
|
|
82
|
+
|
|
83
|
+
在构建时,Next.js 会为每个语言环境生成一个 JS 包(例如 `./locales_fr_12345.js`)。当站点发送到客户端并运行时,浏览器会为所需的 JS 文件发出额外的 HTTP 请求。
|
|
84
|
+
|
|
85
|
+
> 解决同一问题的另一种方法是使用 `fetch()` 动态加载 JSON。这就是当 JSON 存储在 `/public` 下时 `Tolgee` 的工作方式,或者是依赖 `getStaticProps` 加载内容的 `next-translate` 的工作方式。流程是一样的:浏览器会发起额外的 HTTP 请求来下载资源。
|
|
86
|
+
|
|
87
|
+
**内容拆分**
|
|
88
|
+
|
|
89
|
+
如果你使用类似 `const t = useTranslation()` + `t('my-object.my-sub-object.my-key')` 的语法,通常整个 JSON 都必须包含在打包包中,以便库可以解析它并解析键。即使页面上没有用到,大部分内容也会被发送。
|
|
90
|
+
|
|
91
|
+
为了减轻这种情况,一些库要求你声明每个页面要加载哪些命名空间——例如 `next-i18next`、`next-intl`、`lingui`、`next-translate`、`next-international`。
|
|
92
|
+
|
|
93
|
+
相比之下,`Paraglide` 在构建前增加了一个额外步骤,将 JSON 转换为扁平的符号,如 `const en_my_var = () => 'my value'`。理论上,这可以在页面上实现 Tree-shaking 掉未使用的内容。正如我们将看到的,这种方法仍然存在权衡。
|
|
94
|
+
|
|
95
|
+
最后,`Intlayer` 应用了构建时优化,使得 `useIntlayer('my-key')` 会被直接替换为相应的内容。
|
|
96
|
+
|
|
97
|
+
## 方法论
|
|
98
|
+
|
|
99
|
+
在此基准测试中,我们对比了以下库:
|
|
100
|
+
|
|
101
|
+
- `Base App`(无 i18n 库)
|
|
102
|
+
- `next-intlayer` (v8.7.5)
|
|
103
|
+
- `next-i18next` (v16.0.5)
|
|
104
|
+
- `next-intl` (v4.9.1)
|
|
105
|
+
- `@lingui/core` (v5.3.0)
|
|
106
|
+
- `next-translate` (v3.1.2)
|
|
107
|
+
- `next-international` (v1.3.1)
|
|
108
|
+
- `@inlang/paraglide-js` (v2.15.1)
|
|
109
|
+
- `tolgee` (v7.0.0)
|
|
110
|
+
- `@lingo.dev/compiler` (v0.4.0)
|
|
111
|
+
- `wuchale` (v0.22.11)
|
|
112
|
+
- `gt-next` (v6.16.5)
|
|
113
|
+
|
|
114
|
+
我使用了 `Next.js` 版本 `16.2.4` 以及 App Router。
|
|
115
|
+
|
|
116
|
+
我构建了一个拥有 **10 个页面**和 **10 种语言**的多语言应用。
|
|
117
|
+
|
|
118
|
+
我对比了**四种加载策略**:
|
|
119
|
+
|
|
120
|
+
| 策略 | 无命名空间(全局) | 有命名空间(局部/作用域) |
|
|
121
|
+
| :----------- | :--------------------------------- | :----------------------------------------------------------- |
|
|
122
|
+
| **静态加载** | **Static**: 启动时全部加载到内存。 | **Scoped static**: 按命名空间拆分;启动时全部加载。 |
|
|
123
|
+
| **动态加载** | **Dynamic**: 按语言环境按需加载。 | **Scoped dynamic**: 按命名空间和语言环境进行更细粒度的加载。 |
|
|
124
|
+
|
|
125
|
+
## 策略总结
|
|
126
|
+
|
|
127
|
+
- **Static (静态)**: 简单;初始加载后无网络延迟。缺点:打包体积大。
|
|
128
|
+
- **Dynamic (动态)**: 减轻初始重量(懒加载)。适用于语言环境较多的情况。
|
|
129
|
+
- **Scoped static (局部静态)**: 保持代码组织良(逻辑分离),且无需复杂的额外网络请求。
|
|
130
|
+
- **Scoped dynamic (局部动态)**: 代码分割和性能的最佳方案。通过仅加载当前视图和活动语言环境所需的内容来最小化内存占用。
|
|
131
|
+
|
|
132
|
+
### 我测量了什么:
|
|
133
|
+
|
|
134
|
+
我在真实浏览器中为每个技术栈运行了相同的多语言应用,然后记录了网络上传输的内容以及所需的时间。数值以**常规网页压缩后**的体积报告,因为这比原始源码计数更接近用户的实际下载量。
|
|
135
|
+
|
|
136
|
+
- **国际化库体积**: 在打包、Tree-shaking 和压缩后,i18n 库的体积,即一个空组件中 Provider(如 `NextIntlClientProvider`)+ Hook(如 `useTranslations`)代码的体积。这不包括翻译文件的加载。它反映了在引入内容之前,库本身的昂贵程度。
|
|
137
|
+
|
|
138
|
+
- **每页 JavaScript 量**: 对于基准测试中的每条路由,浏览器访问该页面时拉取的脚本量,按套件中的页面进行平均(如果报告有汇总,则按语言环境平均)。臃肿的页面即是慢页面。
|
|
139
|
+
|
|
140
|
+
- **来自其他语言环境的泄漏**: 同一页面但为其他语言的内容被错误地加载到了审计页面中。这些内容是不必要的,应当避免(例如 `/fr/about` 的页面内容出现在 `/en/about` 的页面包中)。
|
|
141
|
+
|
|
142
|
+
- **来自其他路由的泄漏**: 应用中**其他屏幕**的相同情况:当你只打开一个页面时,其他页面的文案是否也被打包了(例如 `/en/about` 的页面内容出现在 `/en/contact` 的页面包中)。得分高意味着分割薄弱或打包范围过广。
|
|
143
|
+
|
|
144
|
+
- **平均组件打包体积**: 通用 UI 组件被**逐个测量**,而不是隐藏在应用的总数据中。这显示了国际化是否会悄悄增加日常组件的体积。例如,如果你的组件重新渲染,它将从内存中加载所有这些数据。给任何组件附加一个巨大的 JSON,就像连接了一个庞大的未使用数据仓库,会降低组件性能。
|
|
145
|
+
|
|
146
|
+
- **语言切换响应性**: 我使用应用自带的控制组件切换语言,并计时直到页面明显完成切换所需的时间——这是访问者能感知到的时间,而非实验室微秒级步骤。
|
|
147
|
+
|
|
148
|
+
- **语言更改后的渲染工作**: 进一步的跟进:一旦切换开始,界面以新语言重新绘制所需的开销。当“感知”时间和框架开销不一致时非常有用。
|
|
149
|
+
|
|
150
|
+
- **页面初始加载时间**: 从导航开始到浏览器认为页面已完全加载所需的时间。适用于冷启动对比。
|
|
151
|
+
|
|
152
|
+
- **注水 (Hydration) 时间**: 当应用暴露该数据时,客户端将服务端 HTML 转换为可交互状态所需的时间。表格中的破折号表示该实现在本基准测试中未提供可靠的注水数值。
|
|
153
|
+
|
|
154
|
+
## 结果详情
|
|
155
|
+
|
|
156
|
+
### 1 — 应当避免的解决方案
|
|
157
|
+
|
|
158
|
+
应当明确避免诸如 `gt-next` 或 `lingo.dev` 之类的解决方案。它们结合了供应商锁定和代码库污染。尽管投入了大量时间尝试实施,但我从未让它们正常工作过——无论是在 TanStack Start 还是 Next.js 上。
|
|
159
|
+
|
|
160
|
+
遇到的问题:
|
|
161
|
+
|
|
162
|
+
**(General Translation)** (`gt-next@6.16.5`):
|
|
163
|
+
|
|
164
|
+
- 对于一个 110kb 的应用,`gt-react` 额外增加了超过 440kb。
|
|
165
|
+
- 第一次使用 General Translation 构建就提示 `Quota Exceeded, please upgrade your plan`(配额超出,请升级计划)。
|
|
166
|
+
- 翻译未渲染;我收到了错误 `Error: <T> used on the client-side outside of <GTProvider>`,这似乎是库的一个 Bug。
|
|
167
|
+
- 在实施 **gt-tanstack-start-react** 时,我还遇到了该库的一个[问题](https://github.com/generaltranslation/gt/issues/1210#event-24510646961):`does not provide an export named 'printAST' - @formatjs/icu-messageformat-parser`,这导致应用崩溃。在报告此问题后,维护者在 24 小时内修复了它。
|
|
168
|
+
- 该库阻塞了 Next.js 页面的静态渲染。
|
|
169
|
+
|
|
170
|
+
**(Lingo.dev)** (`@lingo.dev/compiler@0.4.0`):
|
|
171
|
+
|
|
172
|
+
- AI 配额超出,完全阻塞了构建——这意味着不付钱就无法发布到生产环境。
|
|
173
|
+
- 编译器丢失了近 40% 的翻译内容。我不得不将所有的 `.map` 重写为扁平的组件块才使其工作。
|
|
174
|
+
- 它们的 CLI 存在 Bug,会无故重置配置文件。
|
|
175
|
+
- 构建时,当添加新内容时,它会完全抹除生成的 JSON 文件。结果是几个键的添加可能会导致 300 多个现有键被清除。
|
|
176
|
+
|
|
177
|
+
### 2 — 实验性解决方案
|
|
178
|
+
|
|
179
|
+
**(Wuchale)** (`wuchale@0.22.11`):
|
|
180
|
+
|
|
181
|
+
`Wuchale` 背后的想法很有趣,但尚不可行。我遇到了响应性问题,不得不强制重新渲染 Provider 才能使应用工作。文档也相当不清晰,增加了上手难度。
|
|
182
|
+
|
|
183
|
+
**(Paraglide)** (`@inlang/paraglide-js@2.15.1`):
|
|
184
|
+
|
|
185
|
+
`Paraglide` 提供了一种创新且经过深思熟虑的方法。即便如此,在这次基准测试中,其公司宣称的 Tree-shaking 在我的 Next.js 或 TanStack Start 设置中并未生效。工作流和 DX 比其他选项更复杂。
|
|
186
|
+
就个人而言,我不喜欢每次推送到代码库前都要重新生成 JS 文件,这通过 PR 产生了持续的合并冲突风险。该工具似乎也更关注 Vite 而非 Next.js。
|
|
187
|
+
最后,与其他解决方案相比,Paraglide 不使用存储(如 React Context)来检索当前语言环境以渲染内容。对于解析的每个节点,它都会从 localStorage / Cookie 等请求语言环境。这导致了影响组件响应性的不必要逻辑执行。
|
|
188
|
+
|
|
189
|
+
### 3 — 可接受的解决方案
|
|
190
|
+
|
|
191
|
+
**(Tolgee)** (`tolgee@7.0.0`):
|
|
192
|
+
|
|
193
|
+
`Tolgee` 解决了前面提到的许多问题。我发现它比类似的工具更难采用。它不提供类型安全,这增加了在编译时捕捉缺失键的难度。我不得不使用自己的函数封装 Tolgee 的函数,以添加缺失键检测。
|
|
194
|
+
|
|
195
|
+
**(Next Intl)** (`next-intl@4.9.1`):
|
|
196
|
+
|
|
197
|
+
`next-intl` 是目前最热门的选项,也是 AI Agent 推荐最多的,但在我看来这是错误的。入门很容易,但在实践中,减少泄漏的优化非常复杂。结合动态加载 + 命名空间 + TypeScript 类型会极大降低开发速度。包体积也相当大(`NextIntlClientProvider` + `useTranslations` 约为 13kb,是 `next-intlayer` 的两倍多)。**next-intl** 曾会阻塞 Next.js 页面的静态渲染。它提供了一个名为 `setRequestLocale()` 的辅助工具。对于 `en.json` / `fr.json` 这样的集中式文件,这似乎得到了部分解决,但当内容拆分为 `en/shared.json` / `fr/shared.json` / `es/shared.json` 等命名空间时,静态渲染仍然会失效。
|
|
198
|
+
|
|
199
|
+
**(Next I18next)** (`next-i18next@16.0.5`):
|
|
200
|
+
|
|
201
|
+
`next-i18next` 可能最受欢迎,因为它是 JavaScript 应用中最早的 i18n 解决方案之一。它拥有许多社区插件。它与 `next-intl` 有着相同的重大缺点。包体积特别大(`I18nProvider` + `useTranslation` 约为 18kb,约为 `next-intlayer` 的 3 倍)。
|
|
202
|
+
|
|
203
|
+
消息格式也不同:`next-intl` 使用 ICU MessageFormat,而 `i18next` 使用自己的格式。
|
|
204
|
+
|
|
205
|
+
**(Next International)** (`next-international@1.3.1`):
|
|
206
|
+
|
|
207
|
+
`next-international` 也解决了上述问题,但与 `next-intl` 或 `next-i18next` 差异不大。它包含了用于特定命名空间翻译的 `scopedT()`,但使用它对打包体积几乎没有影响。
|
|
208
|
+
|
|
209
|
+
**(Lingui)** (`@lingui/core@5.3.0`):
|
|
210
|
+
|
|
211
|
+
`Lingui` 常受赞誉。就个人而言,我觉得围绕 `lingui extract` / `lingui compile` 的工作流比其他方案更复杂,且没有明显的优势。我还注意到语法不统一,容易误导 AI(例如 `t()`、`t''`、`i18n.t()`、`<Trans>`)。
|
|
212
|
+
|
|
213
|
+
### 4 — 推荐方案
|
|
214
|
+
|
|
215
|
+
**(Next Translate)** (`next-translate@3.1.2`):
|
|
216
|
+
|
|
217
|
+
如果你喜欢 `t()` 风格的 API,`next-translate` 是我的主要推荐方案。它通过 `next-translate-plugin` 优雅运作,利用 Webpack / Turbopack loader 通过 `getStaticProps` 加载命名空间。它也是这些方案中最轻量的(约 2.5kb)。对于命名空间拆分,在配置中为每个页面或路由定义命名空间的设计非常周到,比 **next-intl** 或 **next-i18next** 等主要替代方案更易于维护。在版本 `3.1.2` 中,我注意到静态渲染无法工作,Next.js 会回退到动态渲染。
|
|
218
|
+
|
|
219
|
+
**(Intlayer)** (`next-intlayer@8.7.5`):
|
|
220
|
+
|
|
221
|
+
出于客观性考量,我不会亲自评价 `next-intlayer`,因为这是我自己的解决方案。
|
|
222
|
+
|
|
223
|
+
### 个人见解
|
|
224
|
+
|
|
225
|
+
此见解纯属个人观点,不影响基准测试结果。在 i18n 领域,常能看到关于 `const t = useTranslation('xx')` + `<>{t('xx.xx')}</>` 模式的共识。
|
|
226
|
+
|
|
227
|
+
在 React 应用中,我个人认为将函数作为 `ReactNode` 注入是一种反模式。它还会增加可避免的复杂性和 JavaScript 执行开销(即使几乎察觉不到)。
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
---
|
|
2
|
+
createdAt: 2026-04-20
|
|
3
|
+
updatedAt: 2026-04-21
|
|
4
|
+
title: 2026 年 TanStack Start 最佳 i18n 解决方案 - 基准测试报告
|
|
5
|
+
description: 对比 react-i18next、use-intl 和 Intlayer 等 TanStack Start 国际化库。关于打包体积、泄漏和响应性的详细性能报告。
|
|
6
|
+
keywords:
|
|
7
|
+
- benchmark
|
|
8
|
+
- i18n
|
|
9
|
+
- intl
|
|
10
|
+
- tanstack
|
|
11
|
+
- 性能
|
|
12
|
+
- intlayer
|
|
13
|
+
slugs:
|
|
14
|
+
- doc
|
|
15
|
+
- benchmark
|
|
16
|
+
- tanstack
|
|
17
|
+
author: Aymeric PINEAU
|
|
18
|
+
applicationTemplate: https://github.com/intlayer-org/benchmark-i18n-tanstack-start-template
|
|
19
|
+
history:
|
|
20
|
+
- version: 8.7.5
|
|
21
|
+
date: 2026-01-06
|
|
22
|
+
changes: "初始化基准测试"
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# TanStack Start i18n 库 — 2026 年基准测试报告
|
|
26
|
+
|
|
27
|
+
本页面是关于 TanStack Start i18n 解决方案的基准测试报告。
|
|
28
|
+
|
|
29
|
+
## 目录
|
|
30
|
+
|
|
31
|
+
<Toc/>
|
|
32
|
+
|
|
33
|
+
## 交互式基准测试
|
|
34
|
+
|
|
35
|
+
<I18nBenchmark framework="tanstack" vertical/>
|
|
36
|
+
|
|
37
|
+
## 结果参考:
|
|
38
|
+
|
|
39
|
+
<iframe
|
|
40
|
+
src="https://intlayer.org/markdown?url=https%3A%2F%2Fraw.githubusercontent.com%2Fintlayer-org%2Fbenchmark-i18n%2Fmain%2Freport%2Fscripts%2Fsummarize-tanstack.md"
|
|
41
|
+
width="100%"
|
|
42
|
+
height="600px"
|
|
43
|
+
style="border:none;">
|
|
44
|
+
</iframe>
|
|
45
|
+
|
|
46
|
+
> https://intlayer.org/markdown?url=https%3A%2F%2Fraw.githubusercontent.com%2Fintlayer-org%2Fbenchmark-i18n%2Fmain%2Freport%2Fscripts%2Fsummarize-tanstack.md
|
|
47
|
+
|
|
48
|
+
查看完整的基准测试仓库[点击这里](https://github.com/intlayer-org/benchmark-i18n/tree/main)。
|
|
49
|
+
|
|
50
|
+
## 简介
|
|
51
|
+
|
|
52
|
+
国际化解决方案是 React 应用中最重型的依赖之一。在 TanStack Start 上,主要风险在于发送了不必要的内容:同个路由的打包包中包含了其他页面和其他语言环境的翻译。
|
|
53
|
+
|
|
54
|
+
随着应用规模的增长,这个问题会使发送到客户端的 JavaScript 体积迅速膨胀,并拖慢导航速度。
|
|
55
|
+
|
|
56
|
+
实际上,在优化程度最低的实现中,国际化后的页面体积可能比无 i18n 的版本重数倍。
|
|
57
|
+
|
|
58
|
+
另一个影响是开发体验(DX):内容声明方式、类型、命名空间组织、动态加载以及语言环境更改时的响应性。
|
|
59
|
+
|
|
60
|
+
## 测试你的应用
|
|
61
|
+
|
|
62
|
+
为了快速发现 i18n 泄漏问题,我建立了一个免费扫描器,你可以在[这里](https://intlayer.org/i18n-seo-scanner)试用。
|
|
63
|
+
|
|
64
|
+
<iframe src="https://intlayer.org/i18n-seo-scanner" width="100%" height="600px" style="border:none;"/>
|
|
65
|
+
|
|
66
|
+
## 问题所在
|
|
67
|
+
|
|
68
|
+
要限制多语言应用的成本,有两个关键手段:
|
|
69
|
+
|
|
70
|
+
- 按页面/命名空间拆分内容,这样在不需要时就不会加载整个字典
|
|
71
|
+
- 仅在需要时动态加载正确的语言环境
|
|
72
|
+
|
|
73
|
+
理解这些方法的技术限制:
|
|
74
|
+
|
|
75
|
+
**动态加载**
|
|
76
|
+
|
|
77
|
+
如果没有动态加载,大多数解决方案会从第一次渲染开始就将消息保留在内存中,这对于路由和语言环境较多的应用来说会产生巨大的开销。
|
|
78
|
+
|
|
79
|
+
采用动态加载意味着需要权衡:初始 JS 减少了,但有时切换语言时会多出一次请求。
|
|
80
|
+
|
|
81
|
+
**内容拆分**
|
|
82
|
+
|
|
83
|
+
围绕 `const t = useTranslation()` + `t('a.b.c')` 构建的语法非常方便,但往往鼓励在运行时保留大型 JSON 对象。除非库提供了真正的按页拆分策略,否则这种模型很难进行 Tree-shaking。
|
|
84
|
+
|
|
85
|
+
## 方法论
|
|
86
|
+
|
|
87
|
+
在此基准测试中,我们对比了以下库:
|
|
88
|
+
|
|
89
|
+
- `Base App`(无 i18n 库)
|
|
90
|
+
- `react-intlayer` (v8.7.5-canary.0)
|
|
91
|
+
- `react-i18next` (v17.0.2)
|
|
92
|
+
- `use-intl` (v4.9.1)
|
|
93
|
+
- `@lingui/core` (v5.3.0)
|
|
94
|
+
- `@inlang/paraglide-js` (v2.15.1)
|
|
95
|
+
- `tolgee` (v7.0.0)
|
|
96
|
+
- `react-intl` (v10.1.1)
|
|
97
|
+
- `wuchale` (v0.22.11)
|
|
98
|
+
- `gt-react` (vlatest)
|
|
99
|
+
- `lingo.dev` (v0.133.9)
|
|
100
|
+
|
|
101
|
+
框架使用 `TanStack Start`,构建了一个拥有 **10 个页面**和 **10 种语言**的多语言应用。
|
|
102
|
+
|
|
103
|
+
我们对比了**四种加载策略**:
|
|
104
|
+
|
|
105
|
+
| 策略 | 无命名空间(全局) | 有命名空间(局部/作用域) |
|
|
106
|
+
| :----------- | :--------------------------------- | :----------------------------------------------------------- |
|
|
107
|
+
| **静态加载** | **Static**: 启动时全部加载到内存。 | **Scoped static**: 按命名空间拆分;启动时全部加载。 |
|
|
108
|
+
| **动态加载** | **Dynamic**: 按语言环境按需加载。 | **Scoped dynamic**: 按命名空间和语言环境进行更细粒度的加载。 |
|
|
109
|
+
|
|
110
|
+
## 策略总结
|
|
111
|
+
|
|
112
|
+
- **Static (静态)**: 简单;初始加载后无网络延迟。缺点:打包体积大。
|
|
113
|
+
- **Dynamic (动态)**: 减轻初始重量(懒加载)。适用于语言环境较多的情况。
|
|
114
|
+
- **Scoped static (局部静态)**: 保持代码组织良(逻辑分离),且无需复杂的额外网络请求。
|
|
115
|
+
- **Scoped dynamic (局部动态)**: 代码分割和性能的最佳方案。通过仅加载当前视图和活动语言环境所需的内容来最小化内存占用。
|
|
116
|
+
|
|
117
|
+
## 结果详情
|
|
118
|
+
|
|
119
|
+
### 1 — 应当避免的解决方案
|
|
120
|
+
|
|
121
|
+
应当明确避开诸如 `gt-react` 或 `lingo.dev` 之类的解决方案。它们结合了供应商锁定和代码库污染。更糟的是:尽管投入了大量时间尝试实施,但我从未让它们在 TanStack Start 上正常工作(类似于 Next.js 下的 `gt-next`)。
|
|
122
|
+
|
|
123
|
+
遇到的问题:
|
|
124
|
+
|
|
125
|
+
**(General Translation)** (`gt-react@latest`):
|
|
126
|
+
|
|
127
|
+
- 对于一个约 110kb 的应用,`gt-react` 额外增加了超过 440kb(参考同个基准测试中 Next.js 实现的量级)。
|
|
128
|
+
- 第一次使用 General Translation 构建就提示 `Quota Exceeded, please upgrade your plan`。
|
|
129
|
+
- 翻译未渲染;我收到了错误 `Error: <T> used on the client-side outside of <GTProvider>`,这似乎是库的一个 Bug。
|
|
130
|
+
- 在实施 **gt-tanstack-start-react** 时,我还遇到了该库的一个[问题](https://github.com/generaltranslation/gt/issues/1210#event-24510646961):`does not provide an export named 'printAST' - @formatjs/icu-messageformat-parser`,这导致应用崩溃。在报告此问题后,维护者在 24 小时内修复了它。
|
|
131
|
+
- 这些库通过 `initializeGT()` 函数使用了一种反模式,阻碍了打包包进行干净的 Tree-shaking。
|
|
132
|
+
|
|
133
|
+
**(Lingo.dev)** (`lingo.dev@0.133.9`):
|
|
134
|
+
|
|
135
|
+
- AI 配额超出(或服务端依赖受阻),使得不付钱的情况下构建或部署极具风险。
|
|
136
|
+
- 编译器丢失了近 40% 的翻译内容。我不得不将所有的 `.map` 重写为扁平的组件块才使其工作。
|
|
137
|
+
- 它们的 CLI 存在 Bug,会无故重置配置文件。
|
|
138
|
+
- 构建时,当有新内容添加时,它会完全抹除生成的 JSON 文件。结果是几个键的改动就可能抹去数百个现有键。
|
|
139
|
+
- 我曾在 TanStack Start 上遇到该库的响应性问题:切换语言环境时,我必须强制重新渲染 Provider 才能使其生效。
|
|
140
|
+
|
|
141
|
+
### 2 — 实验性解决方案
|
|
142
|
+
|
|
143
|
+
**(Wuchale)** (`wuchale@0.22.11`):
|
|
144
|
+
|
|
145
|
+
`Wuchale` 背后的想法很有趣,但尚非可行方案。我遇到了该库的响应性问题,不得不强制重新渲染 Provider 才能让应用在 TanStack Start 上运行。文档也相当模糊,增加了上手难度。
|
|
146
|
+
|
|
147
|
+
### 3 — 可接受的解决方案
|
|
148
|
+
|
|
149
|
+
**(Paraglide)** (`@inlang/paraglide-js@2.15.1`):
|
|
150
|
+
|
|
151
|
+
`Paraglide` 提供了一种创新且经过深思熟虑的方法。即便如此,在此基准测试中,其公司宣称的 Tree-shaking 在我的 Next.js 实现或 TanStack Start 中并未生效。工作流和 DX 也比其他选项更复杂。就个人而言,我不喜欢每次推送到代码库前都要重新生成 JS 文件,这通过 PR 产生了持续的合并冲突风险。
|
|
152
|
+
|
|
153
|
+
**(Tolgee)** (`tolgee@7.0.0`):
|
|
154
|
+
|
|
155
|
+
`Tolgee` 解决了前面提到的许多问题。我发现它比其他采用类似方法的工具更难上手。它不提供类型安全,这极大增加了在编译时捕捉缺失键的难度。我不得不使用自己的 API 封装 Tolgee 的 API 以添加缺失键检测。
|
|
156
|
+
|
|
157
|
+
在 TanStack Start 上我也遇到了响应性问题:切换语言环境时,我必须强制 Provider 重新渲染并订阅语言环境更改事件,切换后的加载才能正常进行。
|
|
158
|
+
|
|
159
|
+
**(use-intl)** (`use-intl@4.9.1`):
|
|
160
|
+
|
|
161
|
+
`use-intl` 是 React 生态中最时髦的“intl”成员(与 `next-intl` 同系),常被 AI Agent 推荐,但在我看来,在性能优先的环境下这是错误的。入门相对简单,但在实践中,优化和限制泄漏的过程相当复杂。同样,结合动态加载 + 命名空间 + TypeScript 类型会极大降低开发速度。
|
|
162
|
+
|
|
163
|
+
在 TanStack Start 上你可以避开 Next.js 特有的陷阱(`setRequestLocale`、静态渲染),但核心问题是一样的:如果没有严格的规范,打包包很快会承载过多消息,而且维护每条路由的命名空间会变得非常痛苦。
|
|
164
|
+
|
|
165
|
+
**(react-i18next)** (`react-i18next@17.0.2`):
|
|
166
|
+
|
|
167
|
+
`react-i18next` 可能最受欢迎,因为它是最早满足 JavaScript 应用 i18n 需求的方案之一。它还针对特定问题拥有广泛的社区插件。
|
|
168
|
+
|
|
169
|
+
尽管如此,它与基于 `t('a.b.c')` 的技术栈有着相同的重大缺点:优化是可能的,但非常耗时,且大型项目容易陷入不良实践(命名空间 + 动态加载 + 类型)。
|
|
170
|
+
|
|
171
|
+
消息格式也不同:`use-intl` 使用 ICU MessageFormat,而 `i18next` 使用自己的格式——如果混合使用它们,会增加工具链或迁移的复杂度。
|
|
172
|
+
|
|
173
|
+
**(Lingui)** (`@lingui/core@5.3.0`):
|
|
174
|
+
|
|
175
|
+
`Lingui` 常受赞誉。就个人而言,我觉得围绕 `lingui extract` / `lingui compile` 的工作流比其他方案更复杂,且在此 TanStack Start 基准测试中没有明显的优势。我还注意到语法不统一,容易误导 AI(例如 `t()`、`t''`、`i18n.t()`、`<Trans>`)。
|
|
176
|
+
|
|
177
|
+
**(react-intl)** (`react-intl@10.1.1`):
|
|
178
|
+
|
|
179
|
+
`react-intl` 是来自 Format.js 团队的高性能实现。但 DX 依然繁琐:`const intl = useIntl()` + `intl.formatMessage({ id: "xx.xx" })` 增加了复杂度和额外的 JavaScript 开销,并将全局 i18n 实例绑定到了 React 树中的许多节点。
|
|
180
|
+
|
|
181
|
+
### 4 — 推荐方案
|
|
182
|
+
|
|
183
|
+
在本次 TanStack Start 基准测试中,没有与 `next-translate`(Next.js 插件 + `getStaticProps`)直接对应的方案。对于那些确实想要 `t()` API 且拥有成熟生态的团队,`react-i18next` 和 `use-intl` 仍是“合理”的选择,但要做好投入大量时间进行优化以避免泄漏的准备。
|
|
184
|
+
|
|
185
|
+
**(Intlayer)** (`react-intlayer@8.7.5-canary.0`):
|
|
186
|
+
|
|
187
|
+
出于客观性考量,我不会亲自评价 `react-intlayer`,因为这是我自己的解决方案。
|
|
188
|
+
|
|
189
|
+
### 个人见解
|
|
190
|
+
|
|
191
|
+
此见解纯属个人观点,不影响基准测试结果。尽管如此,在 i18n 领域,常能看到关于 `const t = useTranslation('xx')` + `<>{t('xx.xx')}</>` 模式的共识。
|
|
192
|
+
|
|
193
|
+
在 React 应用中,我个人认为将函数作为 `ReactNode` 注入是一种反模式。它还会增加可避免的复杂性和 JavaScript 执行开销(即使几乎察觉不到)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intlayer/docs",
|
|
3
|
-
"version": "8.7.5
|
|
3
|
+
"version": "8.7.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Intlayer documentation",
|
|
6
6
|
"keywords": [
|
|
@@ -72,13 +72,13 @@
|
|
|
72
72
|
"watch": "webpack --config ./webpack.config.ts --watch"
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"@intlayer/config": "8.7.5
|
|
76
|
-
"@intlayer/core": "8.7.5
|
|
77
|
-
"@intlayer/types": "8.7.5
|
|
75
|
+
"@intlayer/config": "8.7.5",
|
|
76
|
+
"@intlayer/core": "8.7.5",
|
|
77
|
+
"@intlayer/types": "8.7.5"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
|
-
"@intlayer/api": "8.7.5
|
|
81
|
-
"@intlayer/cli": "8.7.5
|
|
80
|
+
"@intlayer/api": "8.7.5",
|
|
81
|
+
"@intlayer/cli": "8.7.5",
|
|
82
82
|
"@types/node": "25.6.0",
|
|
83
83
|
"@utils/ts-config": "1.0.4",
|
|
84
84
|
"@utils/ts-config-types": "1.0.4",
|
|
@@ -113,6 +113,66 @@ export const docsEntry = {
|
|
|
113
113
|
vi: readLocale('autoFill.md', 'vi'),
|
|
114
114
|
uk: readLocale('autoFill.md', 'uk'),
|
|
115
115
|
} as unknown as Record<LocalesValues, Promise<string>>,
|
|
116
|
+
'./docs/en/benchmark/index.md': {
|
|
117
|
+
en: readLocale('benchmark/index.md', 'en'),
|
|
118
|
+
ru: readLocale('benchmark/index.md', 'ru'),
|
|
119
|
+
ja: readLocale('benchmark/index.md', 'ja'),
|
|
120
|
+
fr: readLocale('benchmark/index.md', 'fr'),
|
|
121
|
+
ko: readLocale('benchmark/index.md', 'ko'),
|
|
122
|
+
zh: readLocale('benchmark/index.md', 'zh'),
|
|
123
|
+
es: readLocale('benchmark/index.md', 'es'),
|
|
124
|
+
de: readLocale('benchmark/index.md', 'de'),
|
|
125
|
+
ar: readLocale('benchmark/index.md', 'ar'),
|
|
126
|
+
it: readLocale('benchmark/index.md', 'it'),
|
|
127
|
+
'en-GB': readLocale('benchmark/index.md', 'en-GB'),
|
|
128
|
+
pt: readLocale('benchmark/index.md', 'pt'),
|
|
129
|
+
hi: readLocale('benchmark/index.md', 'hi'),
|
|
130
|
+
tr: readLocale('benchmark/index.md', 'tr'),
|
|
131
|
+
pl: readLocale('benchmark/index.md', 'pl'),
|
|
132
|
+
id: readLocale('benchmark/index.md', 'id'),
|
|
133
|
+
vi: readLocale('benchmark/index.md', 'vi'),
|
|
134
|
+
uk: readLocale('benchmark/index.md', 'uk'),
|
|
135
|
+
} as unknown as Record<LocalesValues, Promise<string>>,
|
|
136
|
+
'./docs/en/benchmark/nextjs.md': {
|
|
137
|
+
en: readLocale('benchmark/nextjs.md', 'en'),
|
|
138
|
+
ru: readLocale('benchmark/nextjs.md', 'ru'),
|
|
139
|
+
ja: readLocale('benchmark/nextjs.md', 'ja'),
|
|
140
|
+
fr: readLocale('benchmark/nextjs.md', 'fr'),
|
|
141
|
+
ko: readLocale('benchmark/nextjs.md', 'ko'),
|
|
142
|
+
zh: readLocale('benchmark/nextjs.md', 'zh'),
|
|
143
|
+
es: readLocale('benchmark/nextjs.md', 'es'),
|
|
144
|
+
de: readLocale('benchmark/nextjs.md', 'de'),
|
|
145
|
+
ar: readLocale('benchmark/nextjs.md', 'ar'),
|
|
146
|
+
it: readLocale('benchmark/nextjs.md', 'it'),
|
|
147
|
+
'en-GB': readLocale('benchmark/nextjs.md', 'en-GB'),
|
|
148
|
+
pt: readLocale('benchmark/nextjs.md', 'pt'),
|
|
149
|
+
hi: readLocale('benchmark/nextjs.md', 'hi'),
|
|
150
|
+
tr: readLocale('benchmark/nextjs.md', 'tr'),
|
|
151
|
+
pl: readLocale('benchmark/nextjs.md', 'pl'),
|
|
152
|
+
id: readLocale('benchmark/nextjs.md', 'id'),
|
|
153
|
+
vi: readLocale('benchmark/nextjs.md', 'vi'),
|
|
154
|
+
uk: readLocale('benchmark/nextjs.md', 'uk'),
|
|
155
|
+
} as unknown as Record<LocalesValues, Promise<string>>,
|
|
156
|
+
'./docs/en/benchmark/tanstack.md': {
|
|
157
|
+
en: readLocale('benchmark/tanstack.md', 'en'),
|
|
158
|
+
ru: readLocale('benchmark/tanstack.md', 'ru'),
|
|
159
|
+
ja: readLocale('benchmark/tanstack.md', 'ja'),
|
|
160
|
+
fr: readLocale('benchmark/tanstack.md', 'fr'),
|
|
161
|
+
ko: readLocale('benchmark/tanstack.md', 'ko'),
|
|
162
|
+
zh: readLocale('benchmark/tanstack.md', 'zh'),
|
|
163
|
+
es: readLocale('benchmark/tanstack.md', 'es'),
|
|
164
|
+
de: readLocale('benchmark/tanstack.md', 'de'),
|
|
165
|
+
ar: readLocale('benchmark/tanstack.md', 'ar'),
|
|
166
|
+
it: readLocale('benchmark/tanstack.md', 'it'),
|
|
167
|
+
'en-GB': readLocale('benchmark/tanstack.md', 'en-GB'),
|
|
168
|
+
pt: readLocale('benchmark/tanstack.md', 'pt'),
|
|
169
|
+
hi: readLocale('benchmark/tanstack.md', 'hi'),
|
|
170
|
+
tr: readLocale('benchmark/tanstack.md', 'tr'),
|
|
171
|
+
pl: readLocale('benchmark/tanstack.md', 'pl'),
|
|
172
|
+
id: readLocale('benchmark/tanstack.md', 'id'),
|
|
173
|
+
vi: readLocale('benchmark/tanstack.md', 'vi'),
|
|
174
|
+
uk: readLocale('benchmark/tanstack.md', 'uk'),
|
|
175
|
+
} as unknown as Record<LocalesValues, Promise<string>>,
|
|
116
176
|
'./docs/en/bundle_optimization.md': {
|
|
117
177
|
en: readLocale('bundle_optimization.md', 'en'),
|
|
118
178
|
ru: readLocale('bundle_optimization.md', 'ru'),
|