~/ ?.log $
返回文章列表
8 min read
更新于 2026年3月6日

我不知道的 i18next(01)— 命名空间与资源加载

很多人把 i18next 的命名空间当成"翻译文件的分类文件夹",配置一下 ns 数组就完事了。但命名空间决定的不只是文件怎么分,它直接影响了资源的加载粒度、请求数量和内存占用。一个看似简单的配置选择,可能导致首屏多发十几个请求,也可能让翻译资源在内存里无限堆积。

很多人把 i18next 的命名空间当成”翻译文件的分类文件夹”,配置一下 ns 数组就完事了。但命名空间决定的不只是文件怎么分,它直接影响了资源的加载粒度、请求数量和内存占用。一个看似简单的配置选择,可能导致首屏多发十几个请求,也可能让翻译资源在内存里无限堆积。

一、命名空间到底是什么

先看一个典型的 i18next 初始化:

import i18next from 'i18next';
import Backend from 'i18next-http-backend';

i18next.use(Backend).init({
  lng: 'en',
  ns: ['common', 'product', 'user'],
  defaultNS: 'common',
  backend: {
    loadPath: '/locales/{{lng}}/{{ns}}.json',
  },
});

ns 数组里的每一项,对应一个独立的翻译 JSON 文件。使用时通过冒号分隔命名空间和键名:

i18next.t('welcome'); // 从 defaultNS(common)取
i18next.t('product:item.name'); // 从 product 命名空间取

说白了,命名空间就是 i18next 的资源加载单元。每个命名空间是一次独立的加载操作——这个特性既是优势,也是陷阱。

二、一个命名空间 = 一次 HTTP 请求

这里有一个很多人会忽略的细节——i18next 的 Backend 插件对每个命名空间发起独立请求。

假设项目配置了 3 个命名空间、支持 2 种语言,初始化时会发出多少请求?

i18next.init({
  lng: 'zh',
  fallbackLng: 'en',
  ns: ['common', 'product', 'user'],
});

答案是 6 个请求zh/common.jsonzh/product.jsonzh/user.jsonen/common.jsonen/product.jsonen/user.json。因为 fallbackLng 也会被预加载。

如果命名空间增长到 10 个,就是 20 个请求。再加上预加载多语言(preload: ['en', 'zh', 'ja']),请求数呈乘法增长。这是大型项目国际化性能问题的常见根源。

三、按需加载:loadNamespaces 的工作方式

i18next 的 loadNamespaces 方法解决了”初始化时全量加载”的问题:

// 初始化时只加载核心命名空间
i18next.init({
  ns: ['common'],
  defaultNS: 'common',
});

// 进入商品页时,按需加载 product 命名空间
async function enterProductPage() {
  await i18next.loadNamespaces('product');
  console.log(i18next.t('product:item.name'));
}

加载完成后,翻译数据存入 i18next 内部的 resourceStore。这是一个嵌套对象结构:

// resourceStore 的实际结构
{
  en: {
    common:  { welcome: 'Welcome', cancel: 'Cancel' },
    product: { 'item.name': 'Product Name' },
  },
  zh: {
    common:  { welcome: '欢迎', cancel: '取消' },
    product: { 'item.name': '商品名称' },
  },
}

t() 函数的查找路径就是 resourceStore[lng][ns][key]。换句话说,命名空间决定了查找时的第二层索引。

四、缓存机制:默认行为与隐患

i18next 对已加载的命名空间有内置缓存——同一个命名空间不会重复请求:

await i18next.loadNamespaces('product'); // 第一次:发请求
await i18next.loadNamespaces('product'); // 第二次:直接返回,无请求

这个缓存是纯内存的,生命周期等于页面会话。问题在于:它没有上限,也没有过期机制。

在 SPA 应用中,用户可能在一次会话里浏览几十个页面,每个页面加载不同的命名空间。所有翻译数据都驻留在内存里,不会被清理。对于大多数应用这不是问题,但如果翻译文件本身很大(比如法律条款、产品目录),内存占用会持续增长。

如果你只记住一句话,记住这个:i18next 的缓存是”只增不减”的,需要手动管理边界。

五、三种常见的加载策略对比

下面这三种策略适用于不同规模的项目。先看配置,再看各自的取舍:

(1)全量预加载

i18next.init({
  ns: ['common', 'product', 'user', 'checkout', 'admin'],
  preload: ['en', 'zh'],
});

启动时加载所有命名空间和语言。适合翻译量小(<50KB)的项目,简单粗暴,但首屏请求多。

(2)路由级按需加载

// 路由守卫中加载对应命名空间
async function beforeEnter(route) {
  const nsMap = {
    '/product': 'product',
    '/checkout': 'checkout',
    '/admin': 'admin',
  };
  const ns = nsMap[route.path];
  if (ns) await i18next.loadNamespaces(ns);
}

初始化只加载 common,其他跟随路由按需加载。大多数 SPA 项目的最佳实践。

(3)合并文件 + 单次请求

i18next.init({
  backend: {
    loadPath: '/locales/{{lng}}/all.json',
  },
  ns: ['translation'],
});

把所有翻译合并到一个文件里。请求数降到最低,但文件体积大,且无法按需加载。适合翻译量中等、对首屏速度不敏感的场景。

三种策略的对比:

策略首屏请求首屏体积按需能力适用场景
全量预加载小项目、SSR
路由级按需SPA、大型项目
合并文件1中型项目、简单架构

六、实战:自定义缓存控制

如果项目需要更精细的缓存控制(比如限制内存中的命名空间数量),可以实现一个自定义 Backend:

const cache = new Map();
const MAX_NS = 20;

i18next.use({
  type: 'backend',
  read(lng, ns, callback) {
    const key = `${lng}:${ns}`;

    if (cache.has(key)) {
      return callback(null, cache.get(key));
    }

    fetch(`/locales/${lng}/${ns}.json`)
      .then((res) => res.json())
      .then((data) => {
        // 超出上限时淘汰最早的条目
        if (cache.size >= MAX_NS) {
          const oldest = cache.keys().next().value;
          cache.delete(oldest);
        }
        cache.set(key, data);
        callback(null, data);
      })
      .catch((err) => callback(err));
  },
});

这段代码实现了一个简单的 FIFO 淘汰策略。实际项目中可以用 lru-cache 等成熟库替代,关键在于意识到默认缓存的局限性。

需要注意的是,自定义 Backend 替换的只是”数据从哪来”的逻辑,resourceStore 本身仍然会保留所有加载过的数据。如果要真正释放内存,需要配合 i18next.removeResourceBundle(lng, ns) 手动清理:

// 离开页面时清理不再需要的命名空间
function onLeavePage(ns) {
  i18next.removeResourceBundle('en', ns);
  i18next.removeResourceBundle('zh', ns);
}

七、总结

i18next 命名空间的本质是资源加载的最小单元。它决定了请求粒度、缓存边界和内存生命周期。大多数性能问题不是 i18next 本身慢,而是命名空间的加载策略没有匹配项目的实际规模。


本系列其他文章:

相关主题:

share.ts

// 觉得这篇文章有帮助?

// 欢迎分享给更多人

export const  subscribe  =  "/rss.xml" ;