---
createdAt: 2026-09-13
updatedAt: 2026-09-13
title: "vue-i18n vs Intlayer: 2026 Benchmark"
description: vue-i18n and Intlayer measured on the same Vite + Vue 3 app. Library size, per-page JavaScript, content leakage, component size and locale-switch reactivity, with the numbers explained.
keywords:
- vue-i18n
- Intlayer
- Internationalization
- i18n
- Benchmark
- Bundle size
- Blog
- Vue
- Nuxt
- Vite
- JavaScript
slugs:
- blog
- vue-i18n-vs-intlayer-benchmark
author: aymericzip
---
# vue-i18n VS Intlayer | Vue Internationalization (i18n) Benchmark
`vue-i18n` is the reference i18n library for Vue. Intlayer is a compiler-based, component-scoped alternative with a Vue integration (`vue-intlayer`). We already compared their [features and developer experience](https://intlayer.org/blog/vue-i18n-vs-intlayer). This article looks at what each one costs once the app is built.
The data comes from [Benchmark Bloom](https://github.com/intlayer-org/benchmark-bloom), an open-source suite that builds the same application with each library and records what the browser actually downloads and executes.
> **tl;dr**: On the same Vite + Vue 3 app, `vue-i18n` ships **134.9 KB** of gzipped JavaScript per page against **41.3 KB** for the app without i18n. Intlayer ships **57.1 KB**. The `vue-i18n` runtime alone weighs **24.3 KB gzip** (6x Intlayer's 3.9 KB), every page carries **90% of foreign-page strings**, and a component compiled in isolation drags in **196 KB** because it is bound to the global message tree. The `@intlayer/vue-i18n` adapter keeps the `vue-i18n` API and measured **47.0 KB** per page.
## In short
- **vue-i18n** - The de-facto i18n library for Vue 2 / Vue 3 and the core of `@nuxtjs/i18n`. ICU-style messages, SFC `` blocks, `v-t` directive, `d()` / `n()` formatters, large ecosystem. Messages are registered on a global instance at `createI18n()`; lazy loading per locale is a manual `setLocaleMessage()` pattern, and per-route splitting is yours to build.
- **Intlayer** - Component-centric content model. `.content.ts` dictionaries sit next to the component they serve, a build-time compiler (`vite-intlayer`) tree-shakes and lazy-loads them per component and per locale, strict TypeScript types are generated from your content, and missing translations fail at build time. Ships router / SEO helpers, a Visual Editor / CMS and AI-assisted translation.
| Library | GitHub Stars | Total Commits | Last Commit | First Version | NPM Version | NPM Downloads |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `aymericzip/intlayer` | [](https://github.com/aymericzip/intlayer/stargazers) | [](https://github.com/aymericzip/intlayer/commits) | [](https://github.com/aymericzip/intlayer/commits) | April 2024 | [](https://www.npmjs.com/package/intlayer) | [](https://www.npmjs.com/package/intlayer) |
| `intlify/vue-i18n` | [](https://github.com/intlify/vue-i18n/stargazers) | [](https://github.com/intlify/vue-i18n/commits) | [](https://github.com/intlify/vue-i18n/commits) | Dec 2016 | [](https://www.npmjs.com/package/vue-i18n) | [](https://www.npmjs.com/package/vue-i18n) |
> Badges update automatically. Snapshots will vary over time.
## Side-by-side feature comparison
| Feature | `vue-intlayer` (Intlayer) | `vue-i18n` |
| ------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------- |
| **Translations near components** | ✅ Yes, `.content.ts` collocated with each component | ✅ Via SFC `` blocks (optional); global catalogs are the common setup |
| **TypeScript integration** | ✅ Strict types auto-generated from content | ✅ Good typings; strict key safety needs schema typing and discipline |
| **Missing translation detection** | ✅ TypeScript error + build-time error/warning | ⚠️ Runtime fallback + console warning |
| **Rich content (components / Markdown)** | ✅ Direct support | ⚠️ `` component interpolation; Markdown via external plugins |
| **ICU support** | ⚠️ WIP | ✅ Yes |
| **Formatting (dates, numbers, currencies)** | ✅ Intl-based formatters | ✅ `d()` / `n()` with `datetimeFormats` / `numberFormats` |
| **Localized routing** | ✅ Helpers for Vue Router / Nuxt, `getMultilingualUrls` | ⚠️ Not core (`@nuxtjs/i18n` or custom router setup) |
| **SEO helpers (hreflang, sitemap, robots)** | ✅ Built-in helpers | ❌ Not core |
| **Tree-shaking (ship only used content)** | ✅ Per component, per locale, automated by the compiler | ⚠️ Manual: split catalogs, `setLocaleMessage()` per route |
| **Lazy loading** | ✅ `importMode: 'dynamic'` (one line of config) | ✅ Manual `import()` + `setLocaleMessage()` |
| **Purge unused content** | ✅ Dead dictionaries are dropped at build time | ❌ Not built-in |
| **Testing missing translations (CLI / CI)** | ✅ `npx intlayer content test` | ⚠️ Third-party (`vue-i18n-extract`) |
| **AI-powered translation** | ✅ Built-in, uses your own provider keys | ❌ No |
| **Visual Editor / CMS** | ✅ Free Visual Editor + optional CMS | ❌ No (external localization platforms) |
| **MCP server & Agent Skills** | ✅ Yes | ❌ No |
| **Ecosystem / community** | ⚠️ Smaller but growing fast | ✅ Large and mature in the Vue ecosystem |
## The benchmark
### What was measured
The [Benchmark Bloom](https://github.com/intlayer-org/benchmark-bloom) suite builds **the same Vite + Vue 3 application** with each library: **10 pages** (home, about, blog, careers, contact, FAQ, pricing, products, settings, team), **10 locales** (`en`, `fr`, `es`, `de`, `it`, `pt`, `zh`, `ja`, `ko`, `ru`), identical components and identical content. Pages are measured in `en` and `fr`.
Both libraries were tested in the **static** configuration, the one most Vue projects ship: for `vue-i18n`, every locale's JSON imported and passed to `createI18n({ messages })`; for Intlayer, the default `importMode: 'static'`. In that mode Intlayer bundles every locale too, but the compiler still scopes content **per component**, so a page only carries the dictionaries of the components it renders.
For each build, the suite records:
- **Lib size**: gzip size of an empty component that only imports the i18n library. The fixed cost of the runtime.
- **Page JS**: gzip JavaScript downloaded per page, averaged over all pages and locales.
- **Locale leak %**: share of translated strings found in the downloaded JS that belong to a locale the user is **not** viewing (fingerprinted on `en` and `fr`, so 50% means "the other measured locale is fully present"; with 10 locales bundled, the real waste is higher).
- **Page leak %**: share of translated strings found in the downloaded JS that belong to a page the user is **not** on.
- **Component avg**: average gzip size of each component compiled in isolation. Shows how much i18n runtime and catalog a single component drags in.
- **E2E reactivity**: wall-clock time between selecting a new locale and `html[lang]` updating in the DOM (Playwright, 5 iterations).
- **Page load**: `PerformanceNavigationTiming.duration`.
> Numbers below come from the run dated **2026-09-12** with `vue-i18n` 11.4.0 and `intlayer` 9.5.0 / 9.5.1. The test application is deliberately small (a few dozen strings per locale), so leakage percentages describe a **pattern**: they grow with your content while the runtime cost stays fixed.
### Results on Vite + Vue 3
| Library | Strategy | Lib size (gz) | Lib size (min) | Page JS avg (gz) | Locale leak | Page leak | Component avg (gz) | E2E reactivity | Page load |
| ----------------------------- | -------- | ------------: | -------------: | ---------------: | ----------: | --------: | -----------------: | -------------: | --------: |
| **base** (no i18n) | - | 0.0 KB | 0.0 KB | 41.3 KB | 0.0% | - | 1.1 KB | 1.8 ms | 10.8 ms |
| `vue-i18n` | static | 24.3 KB | 83.2 KB | 134.9 KB | 50.0% | 90.0% | 196.0 KB | 2.8 ms | 13.6 ms |
| **`vue-intlayer`** | static | **3.9 KB** | **11.1 KB** | **57.1 KB** | 56.8% | **0.0%** | **7.7 KB** | **4.5 ms** | 13.8 ms |
| `@intlayer/vue-i18n` (compat) | static | 7.9 KB | 23.2 KB | 47.0 KB | 15.0% | 0.0% | 8.4 KB | 1.5 ms | 9.3 ms |
> The base app's page-leak column is left blank: with no i18n library, the fingerprinting picks up hard-coded strings in shared chunks and the number is not meaningful.
**How to read it**
- **Runtime cost.** `vue-i18n` is one of the heaviest runtimes in the whole benchmark: **24.3 KB gzip / 83.2 KB minified** for an empty component that only imports it. `vue-intlayer` costs 3.9 KB gzip. That gap is paid on every page regardless of how many strings you have.
- **Per-page JavaScript.** The app without i18n weighs 41.3 KB. `vue-i18n` more than triples it to **134.9 KB**; Intlayer lands at **57.1 KB**, +15.8 KB, most of which is the ten bundled locales (see the next point).
- **Leakage.** With `createI18n({ messages: { en, fr, ... } })`, every page ships every locale and every page's strings: **50% locale leakage** (on the two fingerprinted locales) and **90% page leakage**. Intlayer's `static` mode also bundles every locale (hence the comparable locale-leak figure) but has **0% page leakage**: a page only pulls the dictionaries of the components it renders. Switching to `importMode: 'dynamic'` removes the locale leakage as well; that configuration was not part of this Vue run.
- **Component size is where the architecture shows.** A component calling `useI18n()` compiles to **196 KB** on average, because `t()` is bound to the global instance that holds every message of every locale. The same component with `useIntlayer()` compiles to **7.7 KB**: it only reaches its own dictionary.
- **Reactivity** is a non-issue for both (2-5 ms). Vue's reactivity system makes locale switching cheap once the messages are in memory.
- **`@intlayer/vue-i18n`**, the drop-in adapter, keeps the `vue-i18n` API and measured **47.0 KB per page** and **8.4 KB per component**, with the application code untouched.
> For reference, the same run measured `fluent-vue` at 171.8 KB per page, 29.7 KB of runtime and 217 KB per component.
## Why the gap? Global instance vs. compiled dictionaries
`vue-i18n` is a runtime. `createI18n()` builds a global instance holding a message tree per locale; `useI18n()` binds each component to it; `t("footer.github")` looks the key up at render time. This is what makes SFC `` blocks, `v-t`, and runtime message loading possible, and it is also why every component's dependency graph includes the whole tree:
```bash
.
├── locales
│ ├── en.json
│ ├── fr.json
│ └── ... # one file per locale, all pages inside
└── src
├── i18n.ts # createI18n({ messages: { en, fr, ... } })
├── main.ts
└── components
└── Footer.vue # const { t } = useI18n(); t("footer.github")
```
Optimizing means **you** split `en.json` into per-route files, **you** call `setLocaleMessage()` in a router guard, and **you** keep the route-to-file map correct as components move. The runtime can't do it for you because it has no idea which keys a component will ask for.
Intlayer moves that knowledge to the build. Content is declared next to the component, and `vite-intlayer` resolves which component imports which dictionary:
```bash
.
├── intlayer.config.ts
└── src
├── main.ts # createApp(App).use(intlayer)
└── components
└── Footer
├── Footer.vue # useIntlayer("footer")
└── Footer.content.ts
```
The compiler emits, per dictionary and per locale, exactly the JSON that component needs, and drops dictionaries nothing imports. Per-route scoping is a consequence of per-component scoping, not a task.
> To also drop the unused locales, set `dictionary.importMode: 'dynamic'` in `intlayer.config.ts`. See the [bundle optimization doc](https://intlayer.org/doc/concept/bundle-optimization).
## Developer experience
### Setup
**vue-i18n**
```ts fileName="src/i18n.ts"
import { createI18n } from "vue-i18n";
import en from "../locales/en.json";
import fr from "../locales/fr.json";
export const i18n = createI18n({
legacy: false,
locale: "en",
fallbackLocale: "en",
messages: { en, fr },
});
```
```ts fileName="src/main.ts"
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { i18n } from "./i18n";
createApp(App).use(router).use(i18n).mount("#app");
```
**Intlayer**
```ts fileName="intlayer.config.ts"
import { type IntlayerConfig, Locales } from "intlayer";
const config: IntlayerConfig = {
internationalization: {
locales: [Locales.ENGLISH, Locales.FRENCH],
defaultLocale: Locales.ENGLISH,
},
};
export default config;
```
```ts fileName="vite.config.ts"
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { intlayer } from "vite-intlayer";
export default defineConfig({
plugins: [intlayer(), vue()],
});
```
```ts fileName="src/main.ts"
import { createApp } from "vue";
import { intlayer } from "vue-intlayer";
import App from "./App.vue";
import router from "./router";
createApp(App).use(intlayer).use(router).mount("#app");
```
### Component
**vue-i18n**
```json fileName="locales/en.json"
{
"counter": {
"label": "Counter",
"increment": "Increment"
}
}
```
```vue fileName="src/components/Counter.vue"
{{ n(count) }}
```
`t('counter.label')` is a string until you type the message schema yourself; a typo renders the key.
**Intlayer**
```ts fileName="src/components/Counter/Counter.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;
```
```vue fileName="src/components/Counter/Counter.vue"
{{ number.value(count) }}
```
`label` and `increment` are typed; a typo is a TypeScript error, a missing French value is a build error.
### Lazy loading per locale
**vue-i18n**
```ts fileName="src/i18n.ts"
import { nextTick } from "vue";
import { createI18n } from "vue-i18n";
export const i18n = createI18n({
legacy: false,
locale: "en",
fallbackLocale: "en",
});
export const loadLocaleMessages = async (locale: string) => {
const messages = await import(`../locales/${locale}.json`);
i18n.global.setLocaleMessage(locale, messages.default);
await nextTick();
i18n.global.locale.value = locale;
};
```
Then call `loadLocaleMessages()` from a router guard, and split `locales/{locale}.json` by route yourself if you want per-page scoping.
**Intlayer**
```ts fileName="intlayer.config.ts"
const config: IntlayerConfig = {
// ...
dictionary: {
importMode: "dynamic",
},
};
```
## Keep the vue-i18n API, get Intlayer's output
`@intlayer/vue-i18n` is a drop-in adapter: `useI18n()`, `t()`, `d()`, `n()`, `{name}` and `{0}` interpolation, pipe plurals (`"car | cars"`), `v-t` and `i18n.global.locale` keep working, served from Intlayer dictionaries compiled by `vite-intlayer`.
```ts fileName="vite.config.ts"
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueI18nVitePlugin from "@intlayer/vue-i18n/plugin";
export default defineConfig({
plugins: [vue(), vueI18nVitePlugin()],
});
```
In the benchmark, the compat build of the same app went from **134.9 KB to 47.0 KB** per page and from **196 KB to 8.4 KB** per component, with the components untouched. Your existing `locales/{locale}.json` can stay the source of truth through the JSON sync plugin.
See the [vue-i18n migration guide](https://intlayer.org/doc/migration/vue-i18n) and the [compatibility doc](https://intlayer.org/doc/compatibility/vue-i18n). Nuxt users have the same path through [`@nuxtjs/i18n` compatibility](https://intlayer.org/doc/compatibility/nuxtjs-i18n).
## When to choose which?
- **Choose vue-i18n** if you want the standard Vue approach, you rely on ICU messages or SFC `` blocks, you already use `@nuxtjs/i18n`, or a translation platform expects centralized JSON. Budget the time to split catalogs and lazy-load per route if bundle size matters.
- **Choose Intlayer** if you want **component-scoped content**, **strict TypeScript**, **build-time missing-key errors**, **zero-effort tree-shaking and lazy loading**, and built-in editorial tooling (Visual Editor, CMS, AI translation, MCP server). Especially relevant for large, modular Vue / Nuxt codebases and design systems.
- **Choose `@intlayer/vue-i18n`** if you are already on `vue-i18n` and want the bundle gains without a rewrite.
## Related comparisons
- [next-intl vs Intlayer](https://intlayer.org/blog/next-intl-vs-intlayer) (same benchmark)
- [i18next vs Intlayer](https://intlayer.org/blog/i18next-vs-intlayer) (same benchmark)
- [Lingui vs Intlayer](https://intlayer.org/blog/lingui-vs-intlayer) (same benchmark)
- [vue-i18n vs Intlayer (features & DX)](https://intlayer.org/blog/vue-i18n-vs-intlayer)
- [Is vue-i18n outdated?](https://intlayer.org/blog/is-vue-i18n-outdated)
## GitHub STARs
GitHub stars are a strong indicator of a project's popularity, community trust, and long-term relevance. While not a direct measure of technical quality, they reflect how many developers find the project useful, follow its progress, and are likely to adopt it.
[](https://star-history.com/#intlify/vue-i18n&aymericzip/intlayer)
## Conclusion
`vue-i18n` is mature, flexible and deeply integrated with Vue. The benchmark shows what its runtime-first design costs on a Vite build: a **24 KB gzip runtime**, **134.9 KB per page** for an app that weighs 41 KB without i18n, **90% foreign-page content** on every page, and components that each reach **196 KB** because they hang off the global message tree.
Intlayer moves the work into the compiler. Per-component dictionaries and dead-content purging are build outputs, not conventions. On the same app: **3.9 KB runtime**, **57.1 KB per page**, **0% page leakage**, components **25x smaller**. And if a rewrite isn't on the table, `@intlayer/vue-i18n` gets most of the way there with the components untouched.
All the raw data, the test apps and the scripts are in the [Benchmark Bloom repository](https://github.com/intlayer-org/benchmark-bloom). Run it yourself.
Refer to the ['Why Intlayer?' doc](https://intlayer.org/doc/why) for more details.