Faça sua pergunta e obtenha um resumo do documento referenciando esta página e o provedor AI de sua escolha
Histórico de versões
- "Adicionar comparativo de estrelas do GitHub"v8.9.818/05/2026
- "Início do benchmark"v8.7.506/01/2026
O conteúdo desta página foi traduzido com uma IA.
Veja a última versão do conteúdo original em inglêsSe você tiver uma ideia para melhorar esta documentação, sinta-se à vontade para contribuir enviando uma pull request no GitHub.
Link do GitHub para a documentaçãoCopiar o Markdown do documento para a área de transferência
Bibliotecas i18n para Next.js - Relatório de Benchmark 2026
Esta página é um relatório de benchmark para soluções i18n no Next.js.
Índice
Benchmark Interativo
Carregamento JSON dinâmico
Carrega as traduções tardiamente em tempo de execução
JSON com escopo (namespacing)
Namespaces de tradução por página
Benchmark de Desempenho I18n
O que é essa métrica?
O tamanho total compactado em gzip do pacote da biblioteca de internacionalização. Inclui apenas o provedor e a lógica de recuperação de conteúdo após o tree-shaking e a minificação.
Por que é importante?
Um tamanho de biblioteca menor reduz a carga útil inicial de JavaScript, resultando em tempos de download e execução mais rápidos no cliente.
Ver como
Referência de resultados:
Ver dados completos do benchmark
Veja o repositório completo do benchmark aqui.
Introdução
As bibliotecas de internacionalização têm um impacto pesado na sua aplicação. O principal risco é carregar conteúdo para cada página e cada idioma quando o usuário visita apenas uma página.
À medida que sua aplicação cresce, o tamanho do bundle pode crescer exponencialmente, o que pode prejudicar visivelmente o desempenho.
Como exemplo, nos piores casos, uma vez internacionalizada, sua página pode acabar ficando quase 4 vezes maior.
Outro impacto das bibliotecas i18n é o desenvolvimento mais lento. Transformar componentes em conteúdo multilíngue em todos os idiomas consome muito tempo.
Como o problema é difícil, existem muitas soluções - algumas focadas na DX (experiência do desenvolvedor), outras no desempenho ou escalabilidade, e assim por diante.
O Intlayer tenta otimizar em todas essas dimensões.
TL;DR
- Intlayer e next-translate: As melhores escolhas para o desempenho do Next.js, oferecendo a menor pegada e o melhor suporte para renderização estática.
- next-intl: A opção mais badalada, mas pesada e complexa de otimizar para grandes aplicações.
- next-i18next: Popular e rico em plugins, mas carrega um peso significativo de bundle (~3× Intlayer).
- Evite: gt-next e lingo.dev devido a problemas graves de desempenho, aprisionamento tecnológico (vendor lock-in) e erros que quebram a build.
Teste sua aplicação
Para trazer à tona esses problemas, construí um scanner gratuito que você pode experimentar aqui.
O problema
Existem duas formas principais de limitar o impacto de uma aplicação multilíngue no seu bundle:
- Dividir seu JSON (ou conteúdo) entre arquivos / variáveis / namespaces para que o bundler possa aplicar tree-shaking no conteúdo não utilizado para uma determinada página
- Carregar dinamicamente o conteúdo da sua página apenas no idioma do usuário
Limitações técnicas para essas abordagens:
Carregamento dinâmico
Mesmo quando você declara rotas como [locale]/page.tsx, com Webpack ou Turbopack, e mesmo que generateStaticParams seja definido, o bundler não trata locale como uma constante estática. Isso significa que ele pode puxar conteúdo para todos os idiomas em cada página. A principal forma de limitar isso é carregar o conteúdo através de um import dinâmico (ex: import('./locales/${locale}.json')).
O que acontece no momento do build é que o Next.js emite um bundle JS por localidade (ex: ./locales_pt_12345.js). Após o site ser enviado para o cliente, quando a página é executada, o navegador realiza uma requisição HTTP extra para o arquivo JS necessário (ex: ./locales_pt_12345.js).
Outra forma de abordar o mesmo problema é usarfetch()para carregar o JSON dinamicamente. É assim que oTolgeefunciona quando o JSON reside em/public, ou onext-translate, que depende dogetStaticPropspara carregar o conteúdo. O fluxo é o mesmo: o navegador faz uma requisição HTTP extra para carregar o recurso.
Divisão de conteúdo (Content splitting)
Se você usar uma sintaxe como const t = useTranslation() + t('meu-objeto.meu-subobjeto.minha-chave'), o JSON inteiro geralmente precisa estar no bundle para que a biblioteca possa analisá-lo e resolver a chave. Grande parte desse conteúdo é enviado mesmo quando não é utilizado na página.
Para mitigar isso, algumas bibliotecas pedem que você declare por página quais namespaces carregar - ex: next-i18next, next-intl, lingui, next-translate, next-international.
Em contrapartida, o Paraglide adiciona um passo extra antes do build para transformar o JSON em símbolos planos como const en_my_var = () => 'meu valor'. Em teoria, isso permite o tree-shaking de conteúdo não utilizado na página. Como veremos, esse método ainda tem seus compromissos.
Finalmente, o Intlayer aplica uma otimização no momento do build para que useIntlayer('minha-chave') seja substituído diretamente pelo conteúdo correspondente.
Metodologia
Para este benchmark, comparamos as seguintes bibliotecas:
Base App(Sem biblioteca i18n)next-intlayer(v8.7.12)next-i18next(v16.0.5)next-intl(v4.9.1)@lingui/core(v5.3.0)next-translate(v3.1.2)next-international(v1.3.1)@inlang/paraglide-js(v2.15.1)@tolgee/react(v7.0.0)@lingo.dev/compiler(v0.4.0)wuchale(v0.22.11)gt-next(v6.16.5)
Utilizei a versão 16.2.4 do Next.js com o App Router.
Construí uma aplicação multilíngue com 10 páginas e 10 idiomas.
Comparei quatro estratégias de carregamento:
| Estratégia | Sem namespaces (global) | Com namespaces (escopado) |
|---|---|---|
| Carregamento estático | Static: Tudo na memória ao iniciar. | Scoped static: Dividido por namespace; tudo carregado ao iniciar. |
| Carregamento dinâmico | Dynamic: Carregamento sob demanda por localidade. | Scoped dynamic: Carregamento granular por namespace e localidade. |
Resumo das estratégias
- Static: Simples; sem latência de rede após o carregamento inicial. Desvantagem: tamanho grande do bundle.
- Dynamic: Reduz o peso inicial (lazy-loading). Ideal quando se tem muitas localidades.
- Scoped static: Mantém o código organizado (separação lógica) sem requisitos complexos de rede extras.
- Scoped dynamic: Melhor abordagem para code splitting e desempenho. Minimiza a memória carregando apenas o que a visualização atual e a localidade ativa precisam.
O que eu medi:
Executei a mesma aplicação multilíngue em um navegador real para cada stack e anotei o que realmente aparecia na rede e quanto tempo as coisas levavam. Os tamanhos são relatados após a compressão web normal, porque isso é mais próximo do que as pessoas realmente baixam.
Tamanho da biblioteca de internacionalização: Após o bundling, tree-shaking e minificação, o tamanho da biblioteca i18n é o tamanho dos provedores (ex:
NextIntlClientProvider) + código dos hooks (ex:useTranslations) em um componente vazio. Não inclui o carregamento dos arquivos de tradução. Responde a quão cara é a biblioteca antes de seu conteúdo entrar em cena.JavaScript por página: Para cada rota de benchmark, quanto script o navegador puxa para aquela visita, com média entre as páginas da suíte (e entre localidades onde o relatório as agrupa). Páginas pesadas são páginas lentas.
Vazamento de outras localidades (Leakage): É o conteúdo da mesma página, mas em outro idioma, que seria carregado por engano na página auditada. Este conteúdo é desnecessário e deve ser evitado (ex: conteúdo da página
/fr/aboutno bundle da página/en/about).Vazamento de outras rotas: A mesma ideia para outras telas na aplicação: se os textos delas estão presentes quando você abriu apenas uma página (ex: conteúdo da página
/en/aboutno bundle da página/en/contact). Uma pontuação alta sugere divisão fraca ou bundles excessivamente amplos.Tamanho médio do bundle por componente: Peças comuns da UI são medidas uma por uma em vez de se esconderem dentro de um número gigante da aplicação. Isso mostra se a internacionalização infla silenciosamente os componentes do dia a dia. Por exemplo, se seu componente renderizar novamente, ele carregará todos esses dados da memória. Anexar um JSON gigante a qualquer componente é como conectar um grande depósito de dados não utilizados que diminuirá o desempenho dos seus componentes.
Responsividade ao trocar de idioma: Eu troco o idioma usando o controle da própria aplicação e cronometro quanto tempo leva até que a página tenha mudado claramente - o que um visitante notaria, não um micro-passo de laboratório.
Trabalho de renderização após uma mudança de idioma: Um acompanhamento mais específico: quanto esforço a interface levou para redesenhar para o novo idioma uma vez que a mudança está em andamento. Útil quando o tempo "sentido" e o custo do framework divergem.
Tempo de carregamento inicial da página: Da navegação até o navegador considerar a página totalmente carregada para os cenários que testei. Bom para comparar arranques a frio (cold starts).
Tempo de hidratação (Hydration): Quando a aplicação o expõe, quanto tempo o cliente gasta transformando o HTML do servidor em algo que você pode realmente clicar. Um traço nas tabelas significa que aquela implementação não forneceu um valor de hidratação confiável neste benchmark.
Estrelas do GitHub
As estrelas do GitHub são um forte indicador da popularidade de um projeto, da confiança da comunidade e da relevância a longo prazo. Embora não sejam uma medida direta da qualidade técnica, elas refletem quantos desenvolvedores consideram o projeto útil, acompanham seu progresso e têm probabilidade de adotá-lo. Para estimar o valor de um projeto, as estrelas ajudam a comparar a tração entre as alternativas e fornecem insights sobre o crescimento do ecossistema.
Resultados em detalhe
1 - Soluções a evitar
Algumas soluções, como gt-next ou lingo.dev, devem claramente ser evitadas. Elas combinam o aprisionamento tecnológico (vendor lock-in) com a poluição da sua base de código. Apesar de passar muitas horas tentando implementá-las, nunca consegui fazê-las funcionar - nem no TanStack Start nem no Next.js.
Problemas encontrados:
(General Translation) ([email protected]):
- Para uma aplicação de 110kb, o
gt-nextadiciona mais de 440kb extras. Quota Exceeded, please upgrade your planlogo no primeiro build com a General Translation.- Traduções não são renderizadas; recebo o erro
Error: <T> used on the client-side outside of <GTProvider>, o que parece ser um bug na biblioteca. - Ao implementar o gt-next, também encontrei um problema con la biblioteca:
does not provide an export named 'printAST' - @formatjs/icu-messageformat-parser, que fazia a aplicação falhar. Após relatar esse problema, o mantenedor corrigiu-o em 24 horas. - A biblioteca bloqueia a renderização estática das páginas do Next.js.
(Lingo.dev) (@lingo.dev/[email protected]):
- Cota de IA excedida, bloqueando a build inteiramente - então você não pode lançar para produção sem pagar.
- O compilador estava perdendo quase 40% do conteúdo traduzido. Tive que reescrever todos os
.mapem blocos de componentes planos para fazer funcionar. - A CLI deles é instável e costumava resetar o arquivo de configuração sem motivo.
- No build, ele apagava totalmente os JSONs gerados quando um novo conteúdo era adicionado. Como resultado, um punhado de chaves poderia apagar mais de 300 chaves existentes.
2 - Soluções experimentais
(Wuchale) ([email protected]):
A ideia por trás do Wuchale é interessante, mas ainda não é viável. Encontrei problemas de reatividade e tive que forçar a re-renderização do provedor para fazer a aplicação funcionar. A documentação também é bastante incerta, o que torna a integração mais difícil.
(Paraglide) (@inlang/[email protected]):
O Paraglide oferece uma abordagem inovadora e bem pensada. Mesmo assim, neste benchmark, o tree-shaking anunciado pela empresa não funcionou para minhas configurações de Next.js ou TanStack Start. O fluxo de trabalho e o DX são mais complexos do que outras opções.
Pessoalmente, não gosto de ter que regenerar arquivos JS antes de cada push, o que cria um risco constante de conflito de merge via PRs. A ferramenta também parece mais focada no Vite do que no Next.js.
Finalmente, em comparação com outras soluções, o Paraglide não usa store (ex: React context) para recuperar a localidade atual para renderizar le conteúdo. Para cada nó analisado, ele solicitará a localidade do localStorage / cookie etc. Isso leva à execução de lógica desnecessária que impacta a reatividade do componente.
Nota sobre paraglide: a solução injeta código na sua base de código para importação, como resultado a métrica 'tamanho da lib' no relatório de benchmark é quase 0. A geração de código é algo bom, porque a função utilizada incluirá apenas a lógica necessária (prefixo total vs sem prefixo, cookie vs armazenamento, etc.). Em comparação, o Intlayer executa essa filtragem via inyeções de variáveis de ambiente na build para forçar o bundler a aplicar tree-shaking ao conteúdo dependendo da lógica. Graças a isso, paraglide e intlayer acabam sendo soluções de 6 a 10 vezes mais leves do que i18next ou next-intl.
3 - Soluções aceitáveis
(Tolgee) (@tolgee/[email protected]):
O Tolgee resolve muitos dos problemas mencionados anteriormente. Achei mais difícil de adotar do que ferramentas semelhantes. Ele não fornece segurança de tipos (type safety), o que também torna mais difícil encontrar chaves ausentes no momento da compilação. Tive que envolver as funções do Tolgee com as minhas para adicionar a detecção de chaves ausentes.
(Next Intl) ([email protected]):
O next-intl é a opção mais badalada e a que os agentes de IA mais recomendam, mas, na minha visão, erradamente. Começar é fácil. Na prática, otimizar para limitar o vazamento é complexo. Combinar carregamento dinâmico + namespaces + tipos TypeScript retarda muito o desenvolvimento. O pacote também é bastante pesado (~13kb para NextIntlClientProvider + useTranslations, que é mais de 2x o next-intlayer). O next-intl costumava bloquear a renderização estática das páginas do Next.js. Ele fornece um auxiliar chamado setRequestLocale(). Isso parece ter sido parcialmente resolvido para arquivos centralizados como en.json / fr.json, mas a renderização estática ainda quebra quando o conteúdo é dividido em namespaces como en/shared.json / fr/shared.json / es/shared.json.
(Next I18next) ([email protected]):
O next-i18next é provavelmente a opção mais popular porque foi uma das primeiras soluções de i18n para aplicações JavaScript. Possui muitos plugins da comunidade. Compartilha os mesmos grandes pontos negativos que o next-intl. O pacote é especialmente pesado (~18kb para I18nProvider + useTranslation, cerca de 3x o next-intlayer).
Os formatos de mensagem também diferem: o next-intl usa ICU MessageFormat, enquanto o i18next usa seu próprio formato.
(Next International) ([email protected]):
O next-international também aborda os problemas acima, mas não difere muito do next-intl ou next-i18next. Inclui scopedT() para traduções específicas de um namespace, mas usá-lo não tem praticamente impacto no tamanho do bundle.
(Lingui) (@lingui/[email protected]):
O Lingui é frequentemente elogiado. Pessoalmente, achei o fluxo de trabalho lingui extract / lingui compile mais complexo que as alternativas, sem uma vantagem clara. Também notei sintaxes inconsistentes que confundem as IAs (ex: t(), t'', i18n.t(), <Trans>).
4 - Recomendações
(Next Translate) ([email protected]):
O next-translate é minha recomendação principal se você gosta de uma API no estilo t(). É elegante via next-translate-plugin, carregando namespaces através de getStaticProps com um carregador Webpack / Turbopack. É também a opção mais leve aqui (~2.5kb). Para os namespaces, definir os namespaces por página ou rota na configuração é bem pensado e mais fácil de manter do que as principais alternativas como next-intl ou next-i18next. Na versão 3.1.2, notei que a renderização estática não funcionava; o Next.js recorria à renderização dinâmica.
(Intlayer) ([email protected]):
Não serei eu a julgar pessoalmente o next-intlayer por uma questão de objetividade, já que é a minha própria solução.
Nota pessoal
Esta nota é pessoal e não afeta os resultados do benchmark. No mundo da internacionalização, muitas vezes vemos um consenso em torno do padrão const t = useTranslation('xx') + <>{t('xx.xx')}</>.
Em aplicações React, injetar uma função como um ReactNode é, na minha visão, um antipadrão. Também adiciona uma complexidade evitável e uma sobrecarga de execução de JavaScript (mesmo que quase imperceptível).