SSR, SSG e ISR no Next.js: o que o Googlebot enxerga
Publicado em
Para o Googlebot, o que importa não é a sigla — é o que vem no HTML da primeira resposta. SSG e SSR entregam o conteúdo completo já nesse HTML e são equivalentes para indexação; ISR entrega HTML completo, possivelmente defasado entre revalidações; CSR entrega um HTML vazio que depende de uma segunda passada de renderização — e é a única das quatro que cria risco real de indexação. Este artigo desloca a comparação de "qual é mais rápida" para "o que o indexador recebe", e termina com o que quase nenhum guia ensina: como provar qual estratégia cada rota sua está usando de verdade.
As quatro estratégias em uma frase cada
- SSG (static generation): o HTML é montado uma vez, no build, e servido pronto de CDN. É o padrão do App Router quando a rota não usa APIs dinâmicas.
- ISR (incremental static regeneration): SSG com prazo de validade — o HTML estático é regerado em background depois de um intervalo de revalidação.
- SSR (server-side rendering): o HTML é montado a cada request, no servidor, com o dado daquele momento.
- CSR (client-side rendering): o servidor manda um esqueleto e o conteúdo é montado no browser, por JavaScript, depois do load.
Repare no critério da lista: quando o HTML ganha conteúdo. É o único eixo que importa para o resto do artigo.
O que o Googlebot realmente faz com o seu HTML
A referência aqui é a documentação de JavaScript SEO do Google Search Central — não achismo de SERP. O processo tem duas filas.
As duas ondas: crawl e render
Na primeira passada, o Googlebot faz o request HTTP, checa o robots.txt,
recebe o HTML e o processa: extrai os links dos href, alimenta a fila de
crawl e indexa o que está ali. Se o seu conteúdo está nesse HTML, o trabalho
essencial acabou aqui.
Toda página com status 200 também entra numa fila de renderização. A doc do Google é explícita sobre o tempo: a página pode ficar nessa fila alguns segundos, mas pode demorar mais — e não há prazo publicado nem garantido. Quando os recursos do Google permitem, um Chromium headless renderiza a página, executa o JavaScript, e o HTML renderizado é usado para (re)indexar e para descobrir links que só existem pós-execução.
O espelho do diagrama:
| Onda | O que enxerga |
|---|---|
| 1 — crawl do HTML | Tudo que veio na primeira resposta do servidor: texto, links, meta tags. Indexação imediata. |
| 2 — renderização (fila) | O que só existe depois de executar JavaScript. Entra quando houver recurso — sem prazo garantido. |
Por que "o Google executa JavaScript" não é o fim da conversa
É verdade que o Google renderiza JavaScript — e é a defesa padrão de quem constrói SPA. Mas a frase esconde três coisas. Primeiro, a renderização é adiada e sem SLA: conteúdo crítico ficar pendurado numa fila de capacidade variável é um risco que HTML pronto não corre. Segundo, a renderização pode falhar parcialmente — script bloqueado, erro de runtime, timeout de API — e o que não renderizou não existe para o índice. Terceiro, e cada vez mais relevante: o Googlebot é praticamente o único crawler que renderiza. Crawlers de outros buscadores e os de sistemas de busca com IA leem o HTML da primeira resposta e param ali. Num site que disputa citação em resposta gerada, conteúdo que só existe pós-hidratação é conteúdo invisível para metade dos leitores-máquina.
SSG: o padrão do App Router (e o padrão certo pra conteúdo)
No App Router, uma rota sem APIs dinâmicas (cookies(), headers(),
searchParams) é estática por padrão. Para rotas dinâmicas como
/blog/[slug], o generateStaticParams() enumera os caminhos no build.
Neste site, os slugs são derivados do próprio conteúdo — o pipeline lê o filesystem em build, e é essa a razão física de a rota ser estática:
// src/app/blog/[slug]/page.tsx
// Somente slugs existentes em /content/blog são gerados; o resto é 404.
export const dynamicParams = false
export function generateStaticParams() {
return getAllPosts().map(({ frontmatter }) => ({ slug: frontmatter.slug }))
}// src/lib/content.ts
/** All blog posts, newest first. Returns [] while content/blog is empty. */
export function getAllPosts(): Post[] {
const dir = path.join(CONTENT_DIR, 'blog')
if (!fs.existsSync(dir)) return []
return fs
.readdirSync(dir)
.filter((f) => f.endsWith('.mdx'))
.map((f) => readMdxFile(path.join(dir, f)))
.sort((a, b) =>
b.frontmatter.datePublished.localeCompare(a.frontmatter.datePublished)
)
}O dynamicParams = false fecha a porta: slug que não existe no conteúdo é
404 no build, não uma tentativa de render em runtime. E os subprodutos de
SEO seguem a mesma lógica — o sitemap derivado do
conteúdo e o feed RSS são rotas estáticas
geradas no mesmo build, então página, sitemap e feed nunca contam histórias
diferentes.
SSR: quando o dado não pode esperar o build
SSR resolve um problema específico: o dado muda por request ou depende de quem pede (preço em tempo real, conteúdo por sessão). O HTML continua chegando completo na primeira resposta — e é isso que importa para o indexador.
SSR não indexa melhor que SSG — indexa igual, mais devagar
Vale dizer com todas as letras, porque a SERP trata SSR como "o modo SEO" do Next.js: para indexação, SSR e SSG são equivalentes. O Googlebot recebe o mesmo tipo de artefato — HTML com conteúdo — nos dois casos. As diferenças são operacionais: SSR paga o custo de montar a página a cada request (TTFB maior, um servidor que precisa estar de pé e aguentar o crawler) em troca de frescor absoluto. Se o dado da página não muda entre deploys, SSR é pagar aluguel por um imóvel que você podia ter comprado no build.
ISR: estático com prazo de validade
Uma ressalva de honestidade antes: este site não usa ISR em produção — é 100% estático, e não tenho medição própria de ISR para mostrar. O que segue é análise de mecanismo, não experiência de campo.
ISR é SSG com revalidate: o HTML estático é servido normalmente e, passado
o intervalo, a próxima visita dispara a regeração em background. Para o
Googlebot, o contrato é o mesmo do SSG — HTML completo na primeira resposta,
indexação sem fila de renderização.
O custo em SEO: a primeira visita depois da revalidação
O trade-off é de frescor. Entre revalidações, qualquer visitante — incluindo o Googlebot — pode receber uma versão defasada. E o modelo stale-while-revalidate significa que a primeira visita depois da expiração ainda recebe a versão antiga, enquanto a nova é montada em background. Para um blog, irrelevante. Para uma página cujo conteúdo precisa refletir rápido no índice (preço, estoque, notícia corrigida), é um atraso estrutural que você aceitou ao escolher a estratégia — eu usaria ISR exatamente onde o dado muda com frequência conhecida e a defasagem de um intervalo é tolerável.
CSR: a única que realmente arrisca a indexação
CSR é o HTML esqueleto: a primeira resposta traz um <div> vazio e um
bundle de JavaScript, e o conteúdo nasce no browser. Todo o peso da
indexação cai na segunda onda — a fila de renderização, sem prazo, sujeita a
falha de execução — e nos crawlers que não renderizam, o conteúdo
simplesmente não existe.
O espelho do diagrama:
| Estratégia | Quando o HTML é montado | O que o Googlebot recebe na primeira resposta |
|---|---|---|
| SSG | no build | conteúdo completo |
| ISR | no build + revalidação | conteúdo completo (possivelmente defasado) |
| SSR | a cada request | conteúdo completo |
| CSR | no browser | esqueleto vazio — o conteúdo depende da fila de renderização |
Como este site é renderizado (e por que TTFB deu 0ms)
Este site é o laboratório dos próprios artigos, então: todas as rotas de página são estáticas, geradas em build a partir do conteúdo em disco — a única exceção do site inteiro é a rota que gera a imagem Open Graph de cada post, renderizada sob demanda, e que nenhum crawler precisa indexar. Uma decisão de performance que só faz sentido nesse cenário está no config:
// next.config.ts
// ── CSS inline (LCP) ───────────────────────────────────────────────────────
// O CSS do site é pequeno (~22KB brutos / ~5KB comprimidos); inlinado no
// <head> elimina o único request render-blocking apontado pelo PSI, ao custo
// de perder o cache do .css entre páginas — troca favorável neste tamanho.
experimental: {
inlineCss: true,
},Dois números reais desse arranjo:
- No PSI mobile de 2026-07-18 em produção, a subparte TTFB do LCP marcou 0ms — a assinatura de HTML estático servido da borda. É um número que SSR, por definição, não entrega, porque sempre há um servidor montando a página entre o request e o primeiro byte. O papel do TTFB dentro do LCP está destrinchado no guia de melhoria de LCP.
- No baseline de 2026-07-20, as 10 URLs do site estavam indexáveis, com
status 200, exatamente um
<h1>cada, JSON-LD válido em todas, zero páginas órfãs e profundidade máxima de 2 cliques.
Como provar qual estratégia sua rota usa
A parte que falta nos guias. Três provas, da mais barata para a mais definitiva.
O mapa de símbolos do next build
Ao fim de todo next build, o Next imprime a lista de rotas com um símbolo
por rota indicando como cada uma foi gerada — e uma legenda logo abaixo
explicando cada símbolo. Leia a legenda do seu próprio build, não a de
um tutorial: ela mudou entre versões do Next e o símbolo que era "estático"
numa versão pode ter outro glifo na sua. A saída real deste site, no
Next.js 16.2.6:
Route (app)
┌ ○ /
├ ○ /_not-found
├ ○ /blog
├ ● /blog/[slug]
│ ├ /blog/gtm-nextjs
│ ├ /blog/inp-nextjs
│ ├ /blog/metadata-api-nextjs
│ └ [+7 more paths]
├ ƒ /blog/-/opengraph-image
├ ○ /busca
├ ○ /feed.xml
├ ○ /ferramentas
├ ○ /ferramentas/gerador-json-ld
├ ○ /guia/seo-tecnico-nextjs
├ ○ /icon.svg
├ ○ /opengraph-image
├ ○ /politica-de-privacidade
├ ○ /robots.txt
├ ○ /sitemap.xml
└ ○ /sobre
○ (Static) prerendered as static content
● (SSG) prerendered as static HTML (uses generateStaticParams)
ƒ (Dynamic) server-rendered on demand
Três símbolos, três estratégias — e repare que a legenda distingue ○ de
●: as duas são estáticas, mas o ● marca as rotas que precisaram de
generateStaticParams() para enumerar os caminhos. Os artigos do blog são
essas.
E aqui vale a honestidade de quem escreve sobre o próprio site: eu
esperava não ver nenhum ƒ nesta lista. A rota
/blog/-/opengraph-image — a imagem Open Graph gerada por post — aparece
como dinâmica, porque ela é a única rota com parâmetro que não declara
generateStaticParams(). Não é um problema de indexação (nenhum crawler
precisa indexar a imagem de card, e ela é servida com cache), mas é
exatamente o tipo de coisa que só a leitura da própria saída de build
revela. É por isso que esta é a primeira das três provas.
Se uma rota que você esperava estática aparece como dinâmica, o build acabou de te contar que alguma API dinâmica vazou para ela — ou que faltou enumerar os caminhos — antes de qualquer crawler descobrir.
curl vs. DOM renderizado
O build diz a intenção; o curl diz o fato. Peça a URL de produção sem
executar JavaScript:
curl -s https://seotecnico.dev.br/blog/ssr-ssg-isr-nextjs | grep -o "<h1[^>]*>[^<]*</h1>"Se o <h1> — e o texto do artigo, e os links internos — estão nessa
resposta, o Googlebot os indexa na primeira onda. Depois compare com o DOM
renderizado (DevTools → Elements): o que existe no DOM mas não existe no
curl é exatamente o conteúdo pendurado na fila de renderização.
Inspeção de URL no Search Console: o HTML testado
A prova definitiva, porque vem do próprio Google: Search Console → Inspeção de URL → "Ver página testada". Ali está o HTML que o Google efetivamente obteve ao testar a página. Confira se o conteúdo crítico está presente — e aproveite a inspeção para validar o JSON-LD da rota, que também precisa estar no HTML da primeira resposta; se você ainda monta o bloco na mão, o gerador de JSON-LD produz o formato que este site usa.
O erro mais comum: o componente que só existe depois da hidratação
O padrão que derruba páginas Next.js bem-intencionadas não é "escolhi SSR em
vez de SSG" — é o componente que busca o próprio conteúdo num useEffect.
A rota é tecnicamente estática, o build sorri, mas o parágrafo importante
nasce no browser: CSR de contrabando dentro de uma página "SSG".
A prova empírica de que este site não carrega esse contrabando está no baseline de 2026-07-20: nas 10 páginas, LCP = FCP — o maior elemento visível pinta junto com o primeiro, porque nada de conteúdo aparece depois da hidratação. Quando LCP e FCP divergem muito num site de conteúdo, essa é uma pista de que algo importante está nascendo tarde — e o que nasce tarde para o usuário, nasce tarde (ou nunca) para o indexador. Como sempre no guia de SEO técnico para Next.js: a estratégia de renderização você declara no código, mas quem confirma é a medição.