Último ano, lançamos um projeto Next.js que ainda me deixa um pouco nervoso quando penso nele. Trinta idiomas. Mais de 91.000 páginas geradas estaticamente. Vercel ISR mantendo tudo atualizado. O tipo de projeto onde uma decisão arquitetônica errada significa que você está olhando para builds de 4 horas, contas de hospedagem de $800/mês, ou -- pior caso -- um site que simplesmente não funciona em coreano.

Esta é a história de como acertamos (e as partes em que não acertamos, no início). Se você está construindo uma aplicação Next.js internacionalizada em grande escala e se perguntando se ISR pode realmente lidar com isso em produção, este artigo é para você.

Next.js i18n at Scale: 30 Languages, 91K Pages, Vercel ISR

Índice

O Problema: Por Que 91K Páginas É Uma Besta Diferente

Deixe-me contextualizar. O cliente era uma marca de e-commerce empresarial expandindo para 30 mercados. Cada mercado precisava de:

  • Páginas de produtos localizadas (~2.800 produtos × 30 locales = 84.000 páginas)
  • Páginas de categoria (~120 categorias × 30 locales = 3.600 páginas)
  • Páginas de marketing orientadas por CMS (~120 × 30 = 3.600 páginas)
  • Total: aproximadamente 91.200 URLs únicas

Com getStaticPaths simples e geração estática completa, o build inicial demoraria algo entre 3 e 5 horas. Não é uma brincadeira. Nós testamos protótipos iniciais e vimos o número subir. Cada deploy significaria horas de risco de downtime, e o time de conteúdo queria publicar atualizações várias vezes por dia.

SSR não era uma opção. Os padrões de tráfego do cliente mostraram picos massivos durante eventos de venda -- estamos falando de 50K usuários simultâneos. Renderizar no servidor 91K variantes de página possíveis sob essa carga exigiria compute séria e introduziria latência que mata taxas de conversão.

ISR foi a resposta. Mas ISR nesta escala tem seu próprio conjunto de desafios que a documentação do Next.js não te prepara.

Decisões Arquitetônicas Que Tomamos Cedo

Antes de escrever uma única linha de código i18n, tomamos três decisões arquitetônicas que nos economizaram meses de dor depois.

Decisão 1: Roteamento de Subpath, Não Domínios

Next.js suporta duas estratégias i18n: roteamento de subpath (/fr/products/...) e roteamento de domínio (fr.example.com). Escolhemos roteamento de subpath. Eis o motivo:

Fator Roteamento de Subpath Roteamento de Domínio
Complexidade DNS/SSL Domínio único 30 domínios/subdomínios para gerenciar
Deploy no Vercel Um projeto Um projeto (mas sobrecarga de configuração de domínio)
Equidade de link SEO Consolidado em um domínio Dividido entre domínios
Eficiência de cache de CDN Melhor (cache de edge compartilhado) Fragmentado
Configuração de analytics Mais simples 30 propriedades ou filtragem complexa

Para a maioria dos projetos com menos de 50 locales, o roteamento de subpath é o caminho. Roteamento de domínio faz sentido quando você precisa de TLDs específicos por país por razões legais ou quando seus mercados têm arquiteturas de conteúdo fundamentalmente diferentes.

Decisão 2: next-intl Sobre next-i18next

Avaliamos ambas as bibliotecas extensivamente. Em 2025, next-intl (v4.x) se tornou a escolha mais forte para projetos App Router, embora estivéssemos no Pages Router para este build. Mesmo no Pages Router, next-intl nos deu:

  • Melhor suporte TypeScript com chaves de mensagem type-safe
  • Bundle de cliente menor (cerca de 2.1KB gzipped vs ~5KB para next-i18next)
  • Suporte nativo para formato de mensagem ICU (plurais, gênero, formatação de números)
  • Configuração mais simples para páginas ISR

Decisão 3: Geração Estática Parcial + ISR

Esta foi a grande. Em vez de tentar gerar estaticamente todas as 91K páginas no momento do build, pré-construímos apenas as páginas com mais tráfego (cerca de 8.000) e deixamos ISR lidar com o resto sob demanda.

// pages/[locale]/products/[slug].tsx
export async function getStaticPaths() {
  // Apenas pré-gera top 100 produtos × top 5 locales
  const topProducts = await getTopProducts(100);
  const primaryLocales = ['en', 'de', 'fr', 'es', 'ja'];
  
  const paths = topProducts.flatMap(product =>
    primaryLocales.map(locale => ({
      params: { slug: product.slug, locale },
    }))
  );

  return {
    paths,
    fallback: 'blocking', // ISR lida com todo o resto
  };
}

Isso reduziu nosso tempo de build de 3+ horas para cerca de 12 minutos. As 83.000 páginas restantes são geradas na primeira requisição e em cache na edge.

Next.js i18n at Scale: 30 Languages, 91K Pages, Vercel ISR - architecture

Configurando Next.js i18n para 30 Locales

A configuração i18n nativa do Next.js em next.config.js lida com detecção e roteamento de locale. Aqui está como nossa configuração se parecia (abreviada):

// next.config.js
const nextConfig = {
  i18n: {
    locales: [
      'en', 'de', 'fr', 'es', 'it', 'pt', 'nl', 'pl', 'cs', 'da',
      'sv', 'fi', 'nb', 'hu', 'ro', 'bg', 'hr', 'sk', 'sl', 'el',
      'tr', 'ja', 'ko', 'zh-CN', 'zh-TW', 'th', 'vi', 'id', 'ms', 'ar'
    ],
    defaultLocale: 'en',
    localeDetection: false, // Nós lidamos com isso nós mesmos
  },
};

Um par de coisas para notar aqui. Desabilitamos localeDetection porque a detecção nativa (baseada em headers Accept-Language) estava causando problemas com caching de ISR. Quando o CDN do Vercel faz cache de uma página, o locale precisa ser determinístico a partir da URL, não de headers. Deixar Next.js fazer redirecionamento automático com base na linguagem do navegador significava cache misses e comportamento inconsistente.

Em vez disso, construímos um middleware customizado de detecção de locale que roda apenas no caminho root:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

const SUPPORTED_LOCALES = ['en', 'de', 'fr', /* ... */];
const DEFAULT_LOCALE = 'en';

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  
  // Apenas redireciona no caminho root
  if (pathname === '/') {
    const acceptLanguage = request.headers.get('accept-language') || '';
    const preferred = acceptLanguage.split(',')[0]?.split('-')[0] || DEFAULT_LOCALE;
    const locale = SUPPORTED_LOCALES.includes(preferred) ? preferred : DEFAULT_LOCALE;
    
    return NextResponse.redirect(new URL(`/${locale}`, request.url));
  }
  
  return NextResponse.next();
}

export const config = {
  matcher: ['/'],
};

Estrutura de Arquivo de Tradução

Com 30 idiomas, o gerenciamento de arquivos de tradução se torna uma preocupação real. Organizamos traduções por namespace:

messages/
├── en/
│   ├── common.json
│   ├── product.json
│   ├── checkout.json
│   └── marketing.json
├── de/
│   ├── common.json
│   ├── product.json
│   └── ...
└── ar/
    └── ...

O payload total de tradução em todos os idiomas foi cerca de 4.2MB. Mas como carregamos traduções por página usando getStaticProps, cada página individual apenas carrega 15-40KB de dados de tradução para seu locale e namespace. Isso é crítico -- você não quer enviar todos os 30 locales para o cliente.

export async function getStaticProps({ locale }: GetStaticPropsContext) {
  return {
    props: {
      messages: {
        ...(await import(`../../messages/${locale}/common.json`)).default,
        ...(await import(`../../messages/${locale}/product.json`)).default,
      },
    },
    revalidate: 300, // ISR: revalida a cada 5 minutos
  };
}

Suporte RTL para Árabe

Árabe era o único idioma RTL em nosso conjunto. Lidamos com isso com um simples wrapper de layout:

const direction = locale === 'ar' ? 'rtl' : 'ltr';

return (
  <html lang={locale} dir={direction}>
    <body className={direction === 'rtl' ? 'font-arabic' : 'font-sans'}>
      {children}
    </body>
  </html>
);

Mais a variante rtl: do Tailwind para ajustes de espaçamento e layout. Isso funcionou surpreendentemente bem -- talvez 5% de nosso CSS precisasse de overrides específicas para RTL.

A Estratégia de ISR Que Realmente Funcionou

ISR (Incremental Static Regeneration) é o herói desta história, mas usá-lo bem em escala requer entender como a infraestrutura do Vercel realmente funciona.

Timing de Revalidação

Usamos diferentes períodos de revalidação dependendo do tipo de conteúdo:

Tipo de Página Período de Revalidação Raciocínio
Páginas de produto 300s (5 min) Preços/estoque mudam frequentemente
Páginas de categoria 900s (15 min) Listagens de produtos atualizam menos frequentemente
Páginas de marketing/CMS 3600s (1 hora) Mudanças de conteúdo são planejadas
Homepage por locale 600s (10 min) Equilíbrio entre atualização e caching

Revalidação Sob Demanda

Para atualizações críticas (mudanças de preço, indisponibilidades de estoque), configuramos revalidação sob demanda via webhook de nosso headless CMS:

// pages/api/revalidate.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { secret, slug, locales } = req.body;
  
  if (secret !== process.env.REVALIDATION_SECRET) {
    return res.status(401).json({ message: 'Invalid secret' });
  }

  try {
    const targetLocales = locales || ['en']; // Padrão para Inglês se não especificado
    
    const revalidations = targetLocales.map((locale: string) =>
      res.revalidate(`/${locale}/products/${slug}`)
    );
    
    await Promise.all(revalidations);
    
    return res.json({ revalidated: true, paths: targetLocales.length });
  } catch (err) {
    return res.status(500).json({ message: 'Error revalidating' });
  }
}

Um detalhe: quando você revalida um produto que existe em 30 locales, você está fazendo 30 chamadas de revalidação. Para uma atualização em massa de 100 produtos, isso são 3.000 requisições de revalidação. Tivemos que adicionar rate limiting e enfileirar essas através de uma função serverless para evitar atingir os limites da API do Vercel.

O Padrão Stale-While-Revalidate

A beleza de ISR é que ela serve conteúdo obsoleto enquanto regenera em segundo plano. Para este projeto, isso significava que usuários sempre recebiam uma resposta rápida (HTML em cache do edge do Vercel), mesmo que os dados tivessem até 5 minutos de atraso. Para um site de e-commerce, esse foi um tradeoff aceitável -- o carrinho e o fluxo de checkout sempre atingem APIs ao vivo para estoque/preço em tempo real.

Pipeline de Conteúdo e Integração de Headless CMS

O conteúdo vivia em um headless CMS (Contentful, neste caso, embora tenhamos feito configurações semelhantes com Sanity e Storyblok para outros clientes -- veja nossos serviços de desenvolvimento de headless CMS para mais sobre isso).

O modelo de localização do Contentful funcionou bem para 30 locales. Cada entrada tem valores de campo específicos de locale, e sua API suporta consulta por locale. Mas há uma consideração de desempenho: buscar um produto com dados de todos os 30 locales é significativamente maior do que buscar um locale.

Sempre consultamos para um único locale em getStaticProps:

const product = await contentfulClient.getEntry(productId, {
  locale: mapToContentfulLocale(locale), // 'en-US', 'de-DE', etc.
  include: 2, // Resolve 2 níveis de entradas vinculadas
});

Isso manteve tempos de resposta da API abaixo de 200ms mesmo para entradas de produtos complexas com múltiplas referências.

Gerenciamento de Traduções

Para traduções de UI (botões, rótulos, mensagens de erro), usamos Crowdin integrado ao nosso repositório Git. O fluxo de trabalho:

  1. Desenvolvedores adicionam novas strings em inglês a messages/en/*.json
  2. Crowdin sincroniza e notifica tradutores
  3. Traduções voltam como PRs
  4. CI valida estrutura e completude de JSON
  5. Traduções ausentes retornam para Inglês

A estratégia de fallback é crítica. Você nunca quer uma página de produção mostrando chaves de tradução como product.add_to_cart. Nossa cadeia de fallback era: locale solicitado → família de idioma (ex: pt-BRpt) → Inglês.

Resultados de Desempenho e Core Web Vitals

Após o lançamento, aqui está o que medimos em todos os 30 locales:

Métrica Alvo Real (P75) Notas
LCP < 2.5s 1.8s Cache hit de ISR
FID < 100ms 45ms JavaScript no lado do cliente mínimo
CLS < 0.1 0.03 Estratégia de carregamento de fonte ajudou
TTFB < 800ms 120ms Edge do Vercel, páginas em cache
TTFB (cache miss) < 2s 1.4s ISR gerando na primeira requisição
Tempo de build < 20min 11min 40s Apenas pré-gerando 8K páginas

Os números de TTFB são a estrela aqui. 120ms para páginas em cache significa que usuários em Tóquio, São Paulo e Frankfurt todos recebem respostas rápidas de nós edge próximos. O 1.4s para cache misses é o tempo de geração de ISR -- aceitável porque acontece apenas uma vez por página por período de revalidação.

Carregamento de Fonte para 30 Idiomas

Um desafio de desempenho específico para sites multilíngues: fontes. Você não pode usar uma única família de fonte para 30 idiomas. Precisávamos de:

  • Latino/Cirílico: Inter (a maioria dos idiomas europeus)
  • Árabe: Noto Sans Arabic
  • CJK: Noto Sans JP/KR/SC/TC
  • Tailandês: Noto Sans Thai

Usar next/font com carregamento de fonte por locale preveniu downloads desnecessários de fonte. Um usuário visitando o site em Japonês apenas baixa Noto Sans JP, não as fontes árabe ou tailandesa.

Análise de Custo no Vercel

Vamos falar sobre dinheiro, porque é aqui que ISR em grande escala fica interessante. Aqui está nossa análise de conta mensal do Vercel em 2025:

Item Custo Mensal Notas
Plano Vercel Pro $20/seat × 4 Plano de time base
Bandwidth (8TB/mo) ~$320 $40/TB após os primeiros 1TB
Execuções de Serverless Function ~$180 Regeneração de ISR + rotas de API
Execuções de Edge Middleware ~$45 Detecção de locale
Escritas de ISR ~$90 Operações de escrita em cache
Total ~$715/mo

Para um site manipulando 2M+ pageviews/mês em 30 locales, $715 é extremamente razoável. A alternativa -- executar SSR em infraestrutura dedicada -- teria custo $2.000-4.000/mês para desempenho e confiabilidade equivalentes.

Uma coisa para observar: custos de escrita de cache ISR podem disparar se você disparar revalidação em massa. Tivemos um incidente onde uma publicação em massa de CMS acionou revalidação para 15.000 páginas simultaneamente. Esse evento único custou cerca de $40 em execuções de função extra. Agora nós fazemos batch de chamadas de revalidação com um delay de 100ms entre elas.

Erros que Cometemos e Como Corrigimos

Eu estaria mentindo se dissesse que isso correu sem problemas desde o primeiro dia. Aqui estão os maiores erros:

Erro 1: Gerando Todos os Locales no Tempo de Build

Nossa primeira abordagem tentou pré-gerar cada página em cada locale. O build rodou por 3 horas e 47 minutos. Depois falhou porque o timeout de build do Vercel (no Pro) é 45 minutos. Mesmo depois de mover para um servidor de build customizado, o processo de deploy era miserável.

Correção: Pré-geração parcial com fallback: 'blocking'. Construa apenas as páginas que mais importam, deixe ISR lidar com a cauda longa.

Erro 2: Não Configurar `fallback` Corretamente

Inicialmente usamos fallback: true em vez de fallback: 'blocking'. A diferença importa: true serve um estado de skeleton/loading na primeira requisição, enquanto blocking aguarda a página gerar. Com true, estávamos tendo erros de hidratação porque nossos componentes de produto esperavam dados que não estavam lá durante a renderização de fallback.

Correção: Mudamos para fallback: 'blocking'. O primeiro visitante para uma página não cacheada aguarda 1-2 segundos, mas todos depois disso recebem a versão em cache instantaneamente.

Erro 3: Tags Hreflang de SEO Estavam Erradas

Este é fácil de errar. Google precisa de tags hreflang para entender o relacionamento entre páginas localizadas. Nossa implementação inicial estava faltando a tag x-default e tinha inconsistências entre as tags <link> e o sitemap XML.

// Implementação correta de hreflang
<Head>
  {locales.map(loc => (
    <link
      key={loc}
      rel="alternate"
      hrefLang={loc}
      href={`https://example.com/${loc}${path}`}
    />
  ))}
  <link rel="alternate" hrefLang="x-default" href={`https://example.com/en${path}`} />
</Head>

Erro 4: Geração de Sitemap

Com 91K URLs, um arquivo sitemap XML único não funciona (o limite do Google é 50.000 URLs por sitemap). Precisávamos de um índice de sitemap com múltiplos sitemaps filhos, divididos por locale:

<!-- sitemap-index.xml -->
<sitemapindex>
  <sitemap><loc>https://example.com/sitemaps/en.xml</loc></sitemap>
  <sitemap><loc>https://example.com/sitemaps/de.xml</loc></sitemap>
  <!-- ... 28 mais -->
</sitemapindex>

Geramos esses usando next-sitemap com configuração customizada, e são regenerados em cada build.

Quando Usar Esta Stack (e Quando Não Usar)

Esta arquitetura -- Next.js + i18n + ISR no Vercel -- é poderosa, mas não é a escolha certa para tudo.

Use quando:

  • Você tem 10+ locales com milhares de páginas
  • Atualizações de conteúdo são frequentes mas não em tempo real
  • Desempenho e Core Web Vitals importam para SEO
  • Seu time conhece bem React/Next.js

Considere alternativas quando:

  • Você tem menos de 5 locales e menos de 1.000 páginas (SSG simples pode ser mais simples)
  • Conteúdo é verdadeiramente em tempo real (trading de ações, placar ao vivo) -- use SSR ou busca no lado do cliente
  • Você é restrito orçamentariamente em hospedagem -- considere Astro para sites multilíngues puramente estáticos a uma fração do custo
  • Seu time é pequeno e não precisa da interatividade do React -- um gerador de site estático com i18n pode ser menos para manter

Para times considerando um projeto assim, ajudamos vários clientes empresariais a arquitetar e construir aplicações Next.js em grande escala. As decisões arquitetônicas nas primeiras duas semanas determinam se o projeto sucede ou se torna um pesadelo de manutenção. Se você quer discutir sua situação específica, entre em contato.

FAQ

Como a roteamento i18n do Next.js funciona com ISR?

A roteamento i18n do Next.js adiciona prefixos de locale a URLs (como /fr/products/shoes). Quando combinado com ISR, cada combinação de locale + página é cacheada independentemente no edge do Vercel. Então /en/products/shoes e /fr/products/shoes são entradas de cache separadas, cada uma com seu próprio timer de revalidação. A função getStaticProps recebe o locale em seu contexto, e você busca as traduções apropriadas e conteúdo localizado lá.

Qual é o número máximo de páginas que ISR do Next.js pode lidar no Vercel?

Não há limite técnico rígido no número de páginas ISR que o Vercel pode servir. Rodamos com sucesso 91K+ páginas, e ouvi falar de projetos com 500K+ páginas. Os limites práticos são tempo de build (para páginas pré-geradas), throughput de revalidação, e custo. O cache de edge do Vercel é projetado para esta escala -- é essencialmente um CDN com invalidação inteligente.

ISR afeta SEO para sites multilíngues?

Não, páginas ISR são HTML totalmente renderizado quando servidas de cache, que é o que crawlers de motor de busca veem. As considerações de SEO-chave são tags hreflang apropriadas, um índice de sitemap bem estruturado com sitemaps por locale, e certificar-se de que sua configuração fallback: 'blocking' previne crawlers de ver páginas incompletas. Google confirmou que páginas de ISR/cacheadas são tratadas igual a HTML estático tradicional.

Como você lida com atualizações de tradução sem reimplantar?

Para conteúdo gerenciado por CMS (descrições de produto, cópia de marketing), traduções atualizam automaticamente através de revalidação de ISR -- ou no timer ou via webhook de revalidação sob demanda. Para traduções de string de UI (rótulos de botão, mensagens de validação de formulário), elas são empacotadas no tempo de build, então requerem um redeploy. Mantemos essas separadas intencionalmente: mudanças de conteúdo nunca devem exigir um deploy, mas mudanças de string de UI passam por code review.

Qual é a diferença de custo entre ISR e SSR para sites multilíngues no Vercel?

SSR executa uma função serverless em cada single requisição. Em 2M pageviews/mês, isso são 2M invocações de função em aproximadamente $0.40 por milhão após o free tier -- cerca de $800/mês apenas em custos de função, mais bandwidth significativamente maior já que há menos caching. Nossa configuração ISR custou cerca de $715/mês total, enquanto SSR equivalente teria sido $2.500-3.500/mês.

Como você lida com diferentes formatos de data, número e moeda em 30 locales?

Usamos a API Intl nativa do navegador através dos utilitários de formatação do next-intl. Isso lida com formatação de data (Intl.DateTimeFormat), formatação de número (Intl.NumberFormat), e exibição de moeda corretamente para cada locale. O formato de mensagem ICU permite você embutir esses formatadores diretamente em strings de tradução: "price": "From {amount, number, ::currency/EUR}". Isso funciona no lado do servidor durante geração de ISR e no lado do cliente para valores dinâmicos.

Devo usar App Router ou Pages Router para i18n em grande escala?

Em Next.js 15 (meados de 2025), a história de i18n do App Router amadureceu significativamente, e next-intl v4 tem excelente suporte a App Router. Para novos projetos, eu recomendaria App Router. Oferece melhor streaming, React Server Components (que reduzem JavaScript no lado do cliente), e controles de cache mais granulares. Nosso projeto usou Pages Router porque foi iniciado em 2024 quando o suporte i18n do App Router era menos estável, mas um projeto greenfield hoje deveria ir App Router.

O que acontece se revalidação de ISR falha? Usuários veem uma página de erro?

Não, e este é um dos melhores recursos de ISR. Se revalidação falha (talvez a API de CMS está down, ou há um erro de código em getStaticProps), Vercel continua servindo a última versão gerada com sucesso da página. Usuários nunca veem um erro -- eles apenas veem conteúdo ligeiramente obsoleto. A revalidação falhada é registrada, e a próxima tentativa de revalidação vai tentar novamente. Isso torna ISR incrivelmente resiliente comparado a SSR, onde uma indisponibilidade de API imediatamente se torna uma indisponibilidade visível ao usuário.