Autor:
    Criação:2026-09-13Última atualização:2026-09-13

    next-intl VS Intlayer | Benchmark de Internacionalização (i18n) do Next.js

    next-intl é a biblioteca i18n mais popular para Next.js. Intlayer é uma alternativa baseada em compilador e com escopo de componente. Ambas localizam uma aplicação App Router. A questão é quanto cada uma custa uma vez que a aplicação está compilada.

    Este artigo não é um tutorial. É uma comparação apoiada por números do Benchmark Bloom, um conjunto de benchmark open-source que constrói a mesma aplicação com cada biblioteca e mede o que o navegador realmente faz download e executa.

    tl;dr: Na mesma aplicação Next.js, next-intl adiciona +12.6 KB gzip de JavaScript em cada página, versus +0.3 KB para Intlayer. Sem trabalho extra, next-intl envia ~90% das strings de páginas estrangeiras com cada página. Alcançar 0% de vazamento com next-intl requer escopo de namespace e pick(messages, [...]) por página. Intlayer alcança 0% por padrão, porque seu compiler escopeia o conteúdo por componente. Se você quer a API next-intl com a saída do Intlayer, o adaptador @intlayer/next-intl mediu 147.5 KB por página versus 153.6 KB com o original.

    Em resumo

    • next-intl - Leve, bem documentado, formato de mensagem ICU, suporte de primeira classe ao App Router com middleware, formatters e helpers de navegação. O conteúdo reside em catálogos JSON centralizados; otimizações de desempenho (namespaces, seleção de mensagens por página, lazy loading) são sua responsabilidade.
    • Intlayer - Modelo de conteúdo centrado em componentes. Dicionários .content.ts ficam ao lado do componente que servem, um compilador em tempo de build faz tree-shake e lazy-loads deles por componente e por locale, tipos TypeScript rigorosos são gerados a partir do seu conteúdo, e traduções ausentes falham em tempo de build. Inclui middleware, helpers de SEO, um Visual Editor / CMS e tradução assistida por IA.
    BibliotecaEstrelas do GitHubTotal de CommitsÚltimo CommitPrimeira VersãoVersão NPMDownloads NPM
    aymericzip/intlayerGitHub Repo starsGitHub commit activityLast CommitAbril 2024npmnpm downloads
    amannn/next-intlGitHub Repo starsGitHub commit activityLast CommitNov 2020npmnpm downloads
    Os emblemas são atualizados automaticamente. Os snapshots variarão ao longo do tempo.

    Comparação de recursos lado a lado

    Recursonext-intlayer (Intlayer)next-intl
    Traduções próximas aos componentes✅ Sim, .content.ts colocado junto com cada componente❌ Não, centralizado em messages/{locale}.json
    Integração TypeScript✅ Tipos estritos gerados automaticamente a partir do conteúdo✅ Bom, chaves tipadas via augmentação global.d.ts
    Detecção de tradução ausente✅ Erro TypeScript + erro/aviso em tempo de build⚠️ Fallback em tempo de execução + aviso no console
    Conteúdo rico (JSX / Markdown / componentes)✅ Suporte direto⚠️ t.rich() / t.markup() com placeholders de tag
    Suporte ICU⚠️ Em desenvolvimento✅ Sim
    Formatação (datas, números, moedas)useNumber, useDate, ... (Intl sob o capô)useFormatter() (Intl sob o capô)
    Roteamento localizado e middleware✅ Proxy/middleware integrado, getMultilingualUrls✅ Middleware integrado, Link, redirect, usePathname
    Auxiliares de SEO (hreflang, sitemap, robots)✅ Auxiliares integrados⚠️ Manual, baseado na configuração de roteamento
    Componentes de servidor síncronouseIntlayer de next-intlayer/server funciona em qualquer componente servidor filho⚠️ getTranslations é assíncrono; filhos síncronos precisam de t passado como props
    Renderização estática✅ Não bloqueia a renderização estática⚠️ Requer setRequestLocale(); catálogos nomeados ainda optaram páginas fora da renderização estática em nossos testes
    Tree-shaking (enviar apenas conteúdo usado)✅ Por componente, por locale, automatizado pelo compilador⚠️ Manual: namespaces + pick(messages, [...]) por página
    Lazy loadingimportMode: 'dynamic' (uma linha de config)⚠️ Importações dinâmicas manuais em getRequestConfig
    Purge unused content✅ Dicionários não utilizados são removidos no momento da compilação❌ Não incluído
    Testing missing translations (CLI / CI)npx intlayer content test⚠️ Não incluído; docs sugerem npx @lingual/i18n-check
    AI-powered translation✅ Incluído, usa suas próprias chaves de provedor❌ Não
    Editor Visual / CMS✅ Editor Visual gratuito + CMS opcional❌ Não (plataformas de localização externas)
    Servidor MCP & Agent Skills✅ Sim❌ Não
    Ecossistema / comunidade⚠️ Menor mas crescendo rapidamente✅ Grande, a referência do Next.js

    O benchmark

    O que foi medido

    O suite Benchmark Bloom constrói a mesma aplicação com cada biblioteca: 10 páginas (home, about, blog, careers, contact, FAQ, pricing, products, settings, team), 10 locales (en, fr, es, de, it, pt, zh, ja, ko, ru), componentes idênticos e conteúdo idêntico. As páginas são medidas em en e fr. Cada biblioteca é implementada em até quatro estratégias de carregamento, da configuração ingênua até a otimizada:

    StrategyDescriptionWho does this
    staticCada locale e cada página agrupadas juntasProtótipos rápidos, código gerado por IA
    dynamicApenas a locale ativa é carregada, mas todas as páginas de uma vezA maioria dos projetos
    scoped-staticNamespaces por rota, sem lazy loadingRaro
    scoped-dynamicNamespaces por rota + lazy loading. Apenas a página atual na locale atual é enviadaApps com orçamento de performance rigoroso

    Intlayer não possui uma variante "scoped": o compilador agrupa conteúdo por componente automaticamente, então suas linhas static e dynamic já estão agrupadas.

    Para cada build, a suite registra:

    • Lib size: tamanho gzip da biblioteca i18n. O custo fixo do runtime.
    • Page JS: JavaScript gzip baixado por página, calculado em média sobre todas as páginas e locales.
    • Locale leak %: proporção de strings traduzidas encontradas no JS baixado que pertencem a um locale que o usuário não está visualizando (fingerprinted em en e fr, então 50% significa "o outro locale medido está totalmente presente"; com 10 locales agrupados, o desperdício real é maior).
    • Page leak %: proporção de strings traduzidas encontradas no JS baixado que pertencem a uma página em que o usuário não está.
    • Component avg: tamanho gzip médio de cada componente compilado isoladamente. Mostra quanto runtime i18n um único componente carrega.
    • E2E reactivity: tempo real entre selecionar uma nova locale e html[lang] atualizar no DOM (Playwright, 5 iterações).
    • Hydration: duração da fase de hidratação do React.
    Os números abaixo vêm da execução datada de 2026-09-12 com next-intl 4.14.2, use-intl 4.14.2 e intlayer 9.5.1. A aplicação de teste é deliberadamente pequena (algumas dezenas de strings por locale), portanto as porcentagens de vazamento descrevem um padrão: eles crescem com seu conteúdo enquanto o custo de runtime permanece fixo.

    Resultados no Next.js (App Router)

    LibraryStrategyLib size (gz)Page JS avg (gz)Locale leakPage leakComponent avg (gz)E2E reactivityHydration
    base (sem i18n)-0.0 KB141.0 KB0.0%0.0%0.9 KB13.4 ms11.8 ms
    next-intlstatic14.7 KB153.6 KB4.2%89.8%21.8 KB16.0 ms14.7 ms
    next-intldynamic14.7 KB153.6 KB9.7%89.9%21.8 KB15.6 ms14.8 ms
    next-intlscoped-static14.7 KB153.6 KB0.0%0.0%80.1 KB17.9 ms17.4 ms
    next-intlscoped-dynamic14.7 KB153.6 KB0.0%0.0%22.9 KB17.8 ms16.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
    @intlayer/next-intl (compat)static8.0 KB147.5 KB0.0%0.0%8.1 KB14.5 ms12.8 ms
    @intlayer/next-intl (compat)dynamic8.0 KB148.7 KB0.0%0.0%8.1 KB11.7 ms12.8 ms

    Como ler isso

    • Custo de runtime. A aplicação base pesa 141.0 KB por página. next-intl a leva para 153.6 KB (+12.6 KB gzip em cada página), Intlayer para 141.3 KB (+0.3 KB). Esta diferença não depende de quantas strings você tem: é o runtime da biblioteca.
    • Vazamento. Nos dois setups que a maioria das equipes realmente implementa (static e dynamic), next-intl entrega ~90% das strings de páginas estrangeiras com cada página: todo o en.json vai para o provider do cliente. Para chegar a 0% é necessário os setups scoped-*: dividir catálogos em namespaces e depois fazer pick() dos corretos em cada página. Intlayer está em 0% em ambas as linhas sem nada disso.
    • O JS por página não se moveu para next-intl entre estratégias. O conteúdo do teste é pequeno, então o vazamento de ~90% é apenas alguns KB aqui. Em uma app real com centenas de strings por página, essa proporção se torna o custo dominante. Enquanto isso, o runtime de +12.6 KB é pago em cada configuração.
    • Tamanho do componente. Um componente que chama useTranslations() compila para 21.8 KB em média; o mesmo componente com useIntlayer() compila para 6.9 KB. Na configuração scoped-static, os componentes next-intl saltam para 80.1 KB porque cada um internaliza seu catálogo de namespace.
    • Reatividade e hidratação estão no mesmo patamar para ambas as bibliotecas no Next.js (15-18 ms). Nenhuma delas é um gargalo aqui.

    Resultados no TanStack Start (use-intl)

    use-intl é o núcleo agnóstico de framework do next-intl. Mesma API, mesmo formato de mensagem. Comparar isso contra intlayer no TanStack Start remove as partes específicas do Next.js da equação.

    LibraryStrategyLib size (gz)Page JS avg (gz)Locale leakPage leakComponent avg (gz)E2E reactivity
    base (sem i18n)-0.0 KB111.0 KB0.0%0.0%0.7 KB8.1 ms
    use-intlstatic14.1 KB179.8 KB50.0%89.8%76.0 KB6.7 ms
    use-intldynamic14.1 KB119.4 KB0.0%89.8%75.9 KB7.0 ms
    use-intlscoped-static14.1 KB128.7 KB0.0%0.0%87.1 KB20.9 ms
    use-intlscoped-dynamic14.1 KB128.7 KB0.0%0.0%87.1 KB13.3 ms
    intlayerstatic5.0 KB125.8 KB50.0%0.0%8.1 KB3.2 ms
    intlayerdynamic5.0 KB118.6 KB0.0%0.0%6.3 KB3.6 ms
    @intlayer/use-intl (compat)dynamic7.3 KB129.7 KB0.0%0.0%9.3 KB8.7 ms

    Como ler isso

    • A configuração ingênua use-intl envia 68.8 KB a mais de JS por página do que a aplicação base, com metade das strings pertencendo à locale errada e 90% à página errada.
    • use-intl em modo dynamic chega a 119.4 KB, próximo ao 118.6 KB do Intlayer, mas ainda carrega 89.8% de vazamento de página: as strings de todas as páginas para a locale ativa são carregadas em cada página. Escopo-las por rota (scoped-*) remove o vazamento, mas custa outro ~9 KB de overhead de chunk.
    • Intlayer's static já tem 0% de vazamento de página: o compilador só agrupa os dicionários usados pelos componentes na página. Habilitando importMode: 'dynamic' (uma linha em intlayer.config.ts) remove também o vazamento de locale.
    • O tamanho do componente é onde a arquitetura se manifesta: 76-87 KB por componente com use-intl versus 6-8 KB com Intlayer. useTranslations() vincula cada componente à árvore de mensagens global; useIntlayer() vincula-o ao seu próprio dicionário.
    • Mudança de locale é 2x-4x mais rápida com Intlayer (3 ms vs 7-21 ms).

    Por que a diferença? Catálogos centralizados vs. dicionários compilados

    next-intl segue o modelo clássico: um JSON por locale, carregado em getRequestConfig, inserido em um NextIntlClientProvider, lido através de t("namespace.key").

    bash
    .
    ├── messages
       ├── en.json
       └── fr.json
    └── src
        ├── i18n
       ├── request.ts
       └── routing.ts
        ├── middleware.ts
        └── app
            └── [locale]
                ├── layout.tsx
                └── about
                    └── page.tsx
    

    O runtime não consegue saber quais keys uma página usará, então o padrão seguro é enviar o catálogo completo. Otimizar significa você dividir o catálogo em namespaces, você decidir quais namespaces cada página precisa, e você manter esse mapeamento sincronizado conforme os componentes se movem. A linha scoped-dynamic do benchmark é a recompensa por esse trabalho, e a maioria das equipes nunca chega lá.

    Intlayer inverte a responsabilidade. O conteúdo é declarado ao lado do componente:

    bash
    .
    ├── intlayer.config.ts
    └── src
        ├── middleware.ts
        ├── app
       └── [locale]
           ├── layout.tsx
           └── about
               ├── page.tsx
               └── page.content.ts
        └── components
            └── Counter
                ├── index.tsx
                └── index.content.ts
    

    No momento da compilação, o compilador (@intlayer/swc / @intlayer/babel) vê qual componente importa qual dicionário. Ele agrupa apenas esses dicionários, apenas para o locale ativo, e descarta os que nada importa. O padrão "scoped-dynamic" torna-se a saída da compilação em vez de uma disciplina que o time tem que manter.

    Para obter os números da linha dynamic, defina dictionary.importMode: 'dynamic' em intlayer.config.ts. Veja a documentação de otimização de bundle.

    Experiência do desenvolvedor

    Componente cliente

    next-intl

    messages/en.json
    {
      "counter": {
        "label": "Counter",
        "increment": "Increment"
      }
    }
    
    src/components/Counter.tsx
    "use client";
    
    import { useState } from "react";
    import { useTranslations, useFormatter } from "next-intl";
    
    export const Counter = () => {
      const t = useTranslations("counter");
      const format = useFormatter();
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>{format.number(count)}</p>
          <button aria-label={t("label")} onClick={() => setCount((c) => c + 1)}>
            {t("increment")}
          </button>
        </div>
      );
    };
    
    Lembrez-se de incluir o namespace counter nas mensagens passadas para NextIntlClientProvider em cada página que renderiza este componente.

    Intlayer

    src/components/Counter/index.content.ts
    import { t, type Dictionary } from "intlayer";
    
    const counterContent = {
      key: "counter",
      content: {
        label: t({ pt: "Contador", en: "Counter", fr: "Compteur" }),
        increment: t({ pt: "Incrementar", en: "Increment", fr: "Incrémenter" }),
      },
    } satisfies Dictionary;
    
    export default counterContent;
    
    src/components/Counter/index.tsx
    "use client";
    
    import { useState } from "react";
    import { useIntlayer } from "next-intlayer";
    import { useNumber } from "next-intlayer/format";
    
    export const Counter = () => {
      // Obter o rótulo e o texto do incremento do conteúdo da internacionalização
      const { label, increment } = useIntlayer("counter");
      // Obter a função de formatação de números
      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>
      );
    };
    

    Nada para registrar na página: o componente traz seu próprio conteúdo.

    Componente de servidor síncrono

    Peças de design-system (navbar, footer, cards) são frequentemente componentes server renderizados como children de componentes client, então não podem ser async.

    next-intl

    src/components/ServerCounter.tsx
    type ServerCounterProps = {
      t: (key: string) => string;
      formattedCount: string;
    };
    
    export const ServerCounter = ({ t, formattedCount }: ServerCounterProps) => (
      <div>
        <p>{formattedCount}</p>
        <button aria-label={t("label")}>{t("increment")}</button>
      </div>
    );
    

    A página tem que await getTranslations("counter") e await getFormatter(), depois passar os resultados para baixo como props. O componente não é mais auto-contido.

    Intlayer

    src/components/ServerCounter.tsx
    import { useIntlayer } from "next-intlayer/server";
    import { useNumber } from "next-intlayer/server/format";
    
    export const ServerCounter = ({ count }: { count: number }) => {
      const { label, increment } = useIntlayer("counter");
      const number = useNumber();
    
      return (
        <div>
          <p>{number(count)}</p>
          <button aria-label={label}>{increment}</button>
        </div>
      );
    };
    

    Metadados

    next-intl

    src/app/[locale]/about/page.tsx
    import type { Metadata } from "next";
    import { getTranslations } from "next-intl/server";
    import { routing } from "@/i18n/routing";
    
    const localizedPath = (locale: string, path: string) =>
      locale === routing.defaultLocale ? path : `/${locale}${path}`;
    
    export const generateMetadata = async ({
      params,
    }: {
      params: Promise<{ locale: string }>;
    }): Promise<Metadata> => {
      const { locale } = await params;
      const t = await getTranslations({ locale, namespace: "about" });
    
      const languages = Object.fromEntries(
        routing.locales.map((l) => [l, localizedPath(l, "/about")])
      );
    
      return {
        title: t("title"),
        description: t("description"),
        alternates: {
          canonical: localizedPath(locale, "/about"),
          languages: { ...languages, "x-default": "/about" },
        },
      };
    };
    

    Intlayer

    src/app/[locale]/about/page.tsx
    import { getIntlayer, getMultilingualUrls } from "intlayer";
    import type { Metadata } from "next";
    import type { LocalPromiseParams } from "next-intlayer";
    
    export const generateMetadata = async ({
      params,
    }: LocalPromiseParams): Promise<Metadata> => {
      const { locale } = await params;
      const metadata = getIntlayer("about-metadata", locale);
      const multilingualUrls = getMultilingualUrls("/about");
    
      return {
        ...metadata,
        alternates: {
          canonical: multilingualUrls[locale as keyof typeof multilingualUrls],
          languages: { ...multilingualUrls, "x-default": "/about" },
        },
      };
    };
    

    Mantenha a API do next-intl, obtenha a saída do Intlayer

    Você não precisa reescrever componentes para obter os números de benchmark acima. @intlayer/next-intl é um adaptador pronto para usar: mantém useTranslations, getTranslations, useFormatter, t.rich(), plurais ICU e os auxiliares next-intl/navigation, e os fornece a partir dos dicionários compilados do Intlayer pelo compilador Intlayer.

    next.config.ts
    import type { NextConfig } from "next";
    import { createNextIntlPlugin } from "@intlayer/next-intl/plugin";
    
    const withIntlayer = createNextIntlPlugin();
    
    const nextConfig: NextConfig = {};
    
    export default withIntlayer(nextConfig);
    

    Na benchmark, a build de compatibilidade da mesma aplicação passou de 153.6 KB para 147.5 KB por página, de 21.8 KB para 8.1 KB por componente, e de ~90% page leakage para 0%, com o código da aplicação intacto. Seus arquivos messages/{locale}.json existentes podem continuar sendo a fonte de verdade através do plugin JSON sync.

    Veja o guia de migração next-intl para o passo a passo.

    Quando escolher qual?

    • Escolha next-intl se você quer o padrão de ecossistema para Next.js, depende de ICU MessageFormat, sua aplicação é pequena a média, ou você se integra com uma plataforma de tradução (Crowdin, Phrase, Lokalise...) que espera JSON centralizado. Considere o tempo para namespace de catálogos e selecione mensagens por página se o desempenho importa.
    • Escolha Intlayer se você quer conteúdo com escopo de componente, TypeScript rigoroso, erros de chaves ausentes em tempo de build, tree-shaking e lazy loading sem esforço, componentes de servidor síncronos, e ferramentas editoriais integradas (Visual Editor, CMS, tradução com IA, servidor MCP). Especialmente relevante para codebases grandes e modulares e design systems.
    • Escolha @intlayer/next-intl se você já está usando next-intl e quer ganhos de bundle sem uma reescrita.

    Comparações relacionadas

    GitHub STARs

    As estrelas do GitHub são um forte indicador de popularidade de um projeto, confiança da comunidade e relevância de longo prazo. Embora não sejam uma medida direta de qualidade técnica, elas refletem quantos desenvolvedores acham o projeto útil, acompanham seu progresso e provavelmente vão adotá-lo.

    Star History Chart

    Conclusão

    next-intl é uma biblioteca sólida e bem mantida, e o benchmark confirma que está longe de ser a pior opção no Next.js. Mas seu modelo de catálogo centralizado coloca cada otimização nas mãos do desenvolvedor: a configuração ingênua vaza ~90% do conteúdo de páginas estrangeiras, e o runtime sozinho custa +12.6 KB gzip em cada página.

    Comentários

    Ainda sem comentários. Seja o primeiro a compartilhar seus pensamentos.

    Artigos relacionados

    Últimos artigos