SSR vs CSR: Quando Cada Abordagem Faz Sentido

Conteúdo técnico toda semana
Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.
Sem spam. Cancele a qualquer momento com 1 clique.
Neste artigo
O custo real de escolher errado
Uma página de marketing renderizada inteiramente no client faz o Googlebot indexar um <div id="root"></div> vazio. Uma dashboard administrativa renderizada no servidor força o backend a montar HTML complexo a cada request, desperdiçando CPU em conteúdo que nenhum crawler vai visitar. As duas decisões são erradas, mas pelos motivos opostos.
A escolha entre SSR e CSR não é filosófica. É uma decisão de engenharia que depende de três variáveis concretas: quem consome a página (crawler, usuário logado, visitante anônimo), com que frequência o conteúdo muda, e qual é o budget de infraestrutura disponível.
Como SSR e CSR funcionam por baixo
No CSR, o servidor entrega um HTML mínimo com um bundle JavaScript. O navegador baixa, parseia e executa esse bundle, que então faz fetch de dados e monta o DOM. O tempo até o usuário ver conteúdo depende do tamanho do bundle, da velocidade da conexão e da potência do dispositivo.
No SSR, o servidor executa o componente React (ou equivalente), gera o HTML completo e envia ao navegador. O usuário vê conteúdo antes de qualquer JavaScript executar. Depois, o processo de hydration conecta os event handlers ao HTML já renderizado.
// next.config.ts — configuração mínima do Next.js App Router
// O App Router usa SSR por padrão para Server Components
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: true,
// poweredByHeader desabilitado para não expor stack ao crawler
poweredByHeader: false,
};
export default nextConfig;A diferença prática: no SSR, o servidor paga o custo de CPU para renderizar. No CSR, o dispositivo do usuário paga esse custo. Essa transferência de trabalho é o trade-off central.
Matriz de decisão: SSR, CSR ou nenhum dos dois
Antes de comparar código, a decisão precisa de critérios explícitos. A tabela abaixo cobre os cenários mais comuns.
| Critério | SSR faz sentido | CSR faz sentido | Considere SSG/ISR |
|---|---|---|---|
| SEO é requisito de negócio | Sim: crawler recebe HTML completo | Não: crawler vê HTML vazio ou parcial | Sim, se conteúdo muda pouco |
| Conteúdo personalizado por usuário | Sim, mas com custo de CPU por request | Sim: dados carregados após autenticação | Não se aplica |
| Time to First Contentful Paint | Menor: HTML chega pronto | Maior: depende do bundle + fetch | Menor ainda: HTML pré-gerado |
| Carga no servidor | Alta: renderiza a cada request | Baixa: serve arquivos estáticos | Mínima: CDN serve cache |
| Interatividade pesada (editor, canvas) | Não compensa: hydration é custosa | Sim: toda lógica roda no client | Não se aplica |
| Dados mudam a cada segundo | Sim, com streaming ou cache curto | Sim, via WebSocket ou polling | Não: cache invalida rápido demais |
| Budget de infra limitado | Cuidado: CPU escala com tráfego | Sim: CDN + API separada é barato | Sim: build-time, sem runtime |
A coluna "Considere SSG/ISR" existe porque SSR e CSR não são as únicas opções. Static Site Generation (SSG) e Incremental Static Regeneration (ISR) resolvem cenários que nenhum dos dois cobre bem.
SSR na prática: página de produto com dados dinâmicos
Um e-commerce precisa que o Googlebot indexe título, preço e descrição. O conteúdo muda quando o estoque atualiza. SSR resolve isso.
// app/products/[slug]/page.tsx — Server Component (Next.js App Router)
// Server Components são SSR por padrão: sem "use client", sem useState
import { notFound } from "next/navigation";
import { getProductBySlug } from "@/lib/products";
import { AddToCartButton } from "@/components/add-to-cart-button";
interface ProductPageProps {
params: Promise<{ slug: string }>;
}
export default async function ProductPage({ params }: ProductPageProps) {
const { slug } = await params;
const product = await getProductBySlug(slug);
if (!product) {
notFound();
}
return (
<article>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Preço renderizado no servidor: crawler indexa o valor real */}
<span aria-label="Preço do produto">
{new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(product.priceInCents / 100)}
</span>
{/* AddToCartButton é Client Component: interatividade isolada */}
<AddToCartButton productId={product.id} />
</article>
);
}// components/add-to-cart-button.tsx — Client Component isolado
// "use client" só onde precisa de interatividade: onClick, useState
"use client";
import { useState } from "react";
interface AddToCartButtonProps {
productId: string;
}
export function AddToCartButton({ productId }: AddToCartButtonProps) {
const [loading, setLoading] = useState(false);
async function handleAddToCart() {
setLoading(true);
await fetch("/api/cart", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ productId, quantity: 1 }),
});
setLoading(false);
}
return (
<button onClick={handleAddToCart} disabled={loading}>
{loading ? "Adicionando..." : "Adicionar ao carrinho"}
</button>
);
}O padrão aqui é cirúrgico: o HTML pesado (título, preço, descrição) vem do servidor. Só o botão interativo é Client Component. Isso minimiza o JavaScript enviado ao navegador e mantém o conteúdo indexável.
CSR na prática: dashboard com estado complexo
Uma dashboard de analytics não precisa de SEO. O usuário está autenticado, os dados mudam a cada interação, e a página tem dezenas de filtros e gráficos. SSR aqui é desperdício: o servidor renderizaria HTML que depende de estado do client (filtros selecionados, período, granularidade).
// app/dashboard/page.tsx — força CSR com dynamic import
// Página wrapper que carrega o dashboard inteiro no client
import dynamic from "next/dynamic";
// ssr: false garante que o componente só renderiza no navegador
// Isso evita que o servidor tente montar gráficos e estados de filtro
const DashboardClient = dynamic(
() => import("@/components/dashboard-client"),
{ ssr: false, loading: () => <DashboardSkeleton /> }
);
function DashboardSkeleton() {
return (
<div role="status" aria-label="Carregando dashboard">
<div style={{ height: 400, background: "#f0f0f0", borderRadius: 8 }} />
</div>
);
}
export default function DashboardPage() {
return (
<main>
<h1>Analytics</h1>
<DashboardClient />
</main>
);
}// components/dashboard-client.tsx — componente 100% client
"use client";
import { useState, useEffect } from "react";
interface MetricData {
date: string;
pageViews: number;
uniqueVisitors: number;
}
export default function DashboardClient() {
const [period, setPeriod] = useState<"7d" | "30d" | "90d">("30d");
const [metrics, setMetrics] = useState<MetricData[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
async function fetchMetrics() {
setLoading(true);
const response = await fetch(`/api/metrics?period=${period}`, {
signal: controller.signal,
});
const data: MetricData[] = await response.json();
setMetrics(data);
setLoading(false);
}
fetchMetrics();
// AbortController cancela o fetch se o período mudar antes da resposta
return () => controller.abort();
}, [period]);
return (
<section>
<nav aria-label="Filtro de período">
{(["7d", "30d", "90d"] as const).map((p) => (
<button
key={p}
onClick={() => setPeriod(p)}
aria-pressed={period === p}
>
{p}
</button>
))}
</nav>
{loading ? (
<p>Carregando métricas...</p>
) : (
<ul>
{metrics.map((m) => (
<li key={m.date}>
{m.date}: {m.pageViews} views, {m.uniqueVisitors} visitantes
</li>
))}
</ul>
)}
</section>
);
}Esse código usa AbortController para cancelar requests em voo quando o filtro muda. Sem isso, respostas fora de ordem causam flickering: o usuário seleciona "7d", depois "30d", e a resposta de "7d" chega por último e sobrescreve os dados de "30d". Esse é um dos bugs clássicos de CSR que passam despercebidos em desenvolvimento local (onde latência é zero) e explodem em produção. Se você quer um tratamento mais completo de fetch resiliente, o post sobre fetch, retry e timeout cobre o assunto em profundidade.
O que NÃO fazer
Anti-pattern 1: "use client" no componente raiz
// ERRADO: transforma a aplicação inteira em CSR
// Todo componente filho herda o contexto de Client Component
"use client";
import { Header } from "@/components/header";
import { ProductList } from "@/components/product-list";
import { Footer } from "@/components/footer";
export default function HomePage() {
return (
<>
<Header />
<ProductList />
<Footer />
</>
);
}O problema: ProductList poderia ser um Server Component que busca dados no servidor sem enviar JavaScript ao navegador. Com "use client" no topo, tudo vira client: o bundle cresce, o SEO sofre, e o servidor não faz nada útil.
// CORRETO: componente raiz é Server Component
// Só marca "use client" nos filhos que precisam de interatividade
import { Header } from "@/components/header";
import { ProductList } from "@/components/product-list";
import { Footer } from "@/components/footer";
// Sem "use client" = Server Component por padrão no App Router
export default function HomePage() {
return (
<>
<Header />
{/* ProductList busca dados no servidor, zero JS no client */}
<ProductList />
<Footer />
</>
);
}Anti-pattern 2: SSR em página que só usuário logado acessa
// ERRADO: renderiza no servidor uma página que o crawler nunca vai ver
// O servidor paga CPU para montar HTML que será descartado se o cookie expirou
export default async function SettingsPage() {
const session = await getSession();
if (!session) redirect("/login");
const preferences = await getUserPreferences(session.userId);
return (
<form>
<label>
Tema
<select defaultValue={preferences.theme}>
<option value="light">Claro</option>
<option value="dark">Escuro</option>
</select>
</label>
{/* 15 campos de preferência renderizados no servidor */}
</form>
);
}Para uma página de configurações com 15 campos de formulário, o SSR gera HTML que o navegador vai substituir assim que o JavaScript hidratar os inputs. O servidor faz trabalho duplicado. Nesse caso, um layout server-rendered com o formulário carregado no client é mais eficiente.
// CORRETO: layout SSR mínimo + formulário CSR
import dynamic from "next/dynamic";
const SettingsForm = dynamic(
() => import("@/components/settings-form"),
{ ssr: false }
);
export default function SettingsPage() {
return (
<main>
<h1>Configurações</h1>
{/* Formulário carrega no client: sem desperdício de CPU no servidor */}
<SettingsForm />
</main>
);
}Esse padrão de separar layout estático (SSR) de conteúdo interativo (CSR) é o mesmo que o Next.js App Router incentiva com a divisão Server/Client Components. Se você está estruturando componentes reutilizáveis, o post sobre Design System com Radix UI e Tailwind mostra como organizar essa fronteira.
Hydration: o custo escondido do SSR
SSR não elimina JavaScript. Ele adia a execução. Depois que o HTML chega, o React precisa "hidratar" a página: percorrer o DOM, comparar com a árvore virtual e conectar event handlers. Em páginas com muitos Client Components, o Time to Interactive (TTI) pode ser pior que CSR puro, porque o usuário vê o conteúdo mas não consegue interagir até a hydration terminar.
// Medindo hydration time no client
"use client";
import { useEffect } from "react";
export function HydrationMarker() {
useEffect(() => {
// performance.mark registra o momento exato em que o useEffect roda
// Isso acontece DEPOIS da hydration completar
performance.mark("hydration-complete");
const navigationEntry = performance.getEntriesByType(
"navigation"
)[0] as PerformanceNavigationTiming;
const hydrationTime =
performance.getEntriesByName("hydration-complete")[0].startTime -
navigationEntry.responseEnd;
// Se hydration > 200ms em mobile, considere reduzir Client Components
console.log(`Hydration: ${hydrationTime.toFixed(1)}ms`);
}, []);
return null;
}Se a hydration passa de 200ms em dispositivos móveis, o caminho é reduzir a superfície de Client Components. O React Server Components do App Router ajudam exatamente nisso: componentes que não precisam de interatividade nunca enviam JavaScript ao navegador. Para validar que essa métrica não regride, testes automatizados com Playwright permitem medir performance em CI.
Streaming SSR: o meio-termo que funciona
O Next.js App Router suporta streaming SSR com Suspense. O servidor envia o shell da página imediatamente e "preenche" seções conforme os dados ficam prontos. Isso reduz o Time to First Byte (TTFB) sem sacrificar SEO.
// app/catalog/page.tsx — streaming com Suspense
import { Suspense } from "react";
import { ProductGrid } from "@/components/product-grid";
import { RecommendationBar } from "@/components/recommendation-bar";
export default function CatalogPage() {
return (
<main>
<h1>Catálogo</h1>
{/* ProductGrid resolve rápido: query simples ao banco */}
<Suspense fallback={<p>Carregando produtos...</p>}>
<ProductGrid />
</Suspense>
{/* RecommendationBar depende de ML service lento */}
{/* O servidor envia o HTML de ProductGrid primeiro */}
{/* RecommendationBar chega via streaming quando o service responde */}
<Suspense fallback={<p>Calculando recomendações...</p>}>
<RecommendationBar />
</Suspense>
</main>
);
}O Suspense boundary define a granularidade do streaming. Cada boundary é um chunk independente. Se RecommendationBar depende de um serviço de ML que demora 800ms, o usuário já vê o grid de produtos enquanto espera. Sem streaming, o TTFB seria 800ms para a página inteira.
Quando SSG e ISR resolvem melhor que SSR e CSR
Blog posts, documentação, landing pages, páginas institucionais: conteúdo que muda raramente e precisa de SEO. SSR desperdiça CPU renderizando o mesmo HTML a cada request. CSR desperdiça o tempo do usuário esperando JavaScript. SSG gera HTML no build e serve via CDN.
ISR é o ponto intermediário: gera HTML estático, mas revalida em background após um intervalo. Uma página de produto que muda a cada hora pode usar ISR com revalidate: 3600.
// app/blog/[slug]/page.tsx — ISR com revalidação de 1 hora
import { getPostBySlug, getAllPostSlugs } from "@/lib/blog";
import { notFound } from "next/navigation";
// generateStaticParams gera as páginas no build
// Slugs novos são gerados on-demand no primeiro acesso
export async function generateStaticParams() {
const slugs = await getAllPostSlugs();
return slugs.map((slug) => ({ slug }));
}
export const revalidate = 3600; // revalida a cada 1 hora
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<time dateTime={post.publishedAt}>{post.publishedAt}</time>
<div dangerouslySetInnerHTML={{ __html: post.htmlContent }} />
</article>
);
}Para APIs que alimentam essas páginas, a camada de fetch entre client e servidor precisa ser resiliente. O post sobre API layers em aplicações fullstack detalha como estruturar essa comunicação com tipagem ponta a ponta.
Segurança: SSR expõe o servidor, CSR expõe o client
SSR executa código no servidor a cada request. Se o componente faz query ao banco, uma injeção de SQL afeta o servidor diretamente. CSR move a lógica para o navegador, mas expõe endpoints de API que precisam de validação rigorosa.
Em ambos os casos, a superfície de ataque existe: muda o vetor. Server Components que acessam banco devem usar queries parametrizadas. APIs consumidas por CSR precisam de rate limiting, validação de payload e autenticação. O post sobre segurança em APIs Node.js com OWASP Top 10 cobre esses vetores em detalhe.
Feature flags podem controlar qual estratégia de renderização uma página usa, permitindo migrar gradualmente de CSR para SSR (ou vice-versa) sem deploy arriscado. Se isso interessa, a implementação de feature flags sem vendor lock-in mostra como fazer isso com TypeScript puro.
Posição editorial
A maioria dos projetos Next.js deveria começar com Server Components como padrão e adicionar "use client" cirurgicamente, componente por componente, conforme a interatividade exigir. Essa abordagem tem o melhor custo-benefício: SEO funciona sem configuração extra, o bundle de JavaScript é menor por padrão, e o servidor faz o trabalho pesado onde tem mais recurso (CPU previsível, rede rápida ao banco).
CSR puro faz sentido em dois cenários: aplicações 100% autenticadas sem necessidade de SEO (dashboards, ferramentas internas, editores) e SPAs que rodam offline ou em ambientes onde o servidor é apenas uma API REST. Fora desses casos, CSR puro é uma escolha que você vai pagar com retrabalho quando o time de marketing pedir indexação ou quando o Core Web Vitals cair.
SSR puro para todas as páginas é igualmente problemático. O custo de CPU escala linearmente com tráfego, e hydration pesada anula a vantagem do HTML rápido. A resposta correta é quase sempre uma combinação: Server Components para conteúdo, Client Components para interação, ISR para conteúdo que muda devagar, streaming para páginas com dependências lentas.
Se você está começando um projeto hoje com Next.js App Router, use o padrão do framework. Ele já faz a escolha certa na maioria dos casos. Intervenha só quando medir um problema real.
Checklist de decisão por tipo de página
Antes de definir a estratégia de renderização, passe cada rota pelo crivo:
- Crawlers precisam ver o conteúdo? Se sim, SSR ou SSG. Se não, CSR.
- O conteúdo muda a cada request? Se sim, SSR com cache curto ou CSR com fetch no cliente. Se não, SSG com revalidação.
- A página tem interatividade pesada? Se sim, minimize a superfície de hidratação com Server Components + Client Components pontuais.
- O tempo de resposta do servidor é previsível? Se o backend é lento (>500ms), use streaming SSR com Suspense para não bloquear o FCP.
- Quantos requests/segundo essa rota recebe? Se é alta frequência e o conteúdo é o mesmo para todos os usuários, SSG/ISR economiza CPU. Se cada usuário vê dados diferentes, CSR transfere o custo para o navegador.
Para quem está montando a infraestrutura de testes dessas rotas, o post sobre testes automatizados com Vitest, MSW e Playwright mostra como testar Server Components e Client Components de forma isolada. E se a API por trás dessas páginas precisa de validação séria, o guia de segurança em APIs Node.js com OWASP Top 10 cobre o lado do servidor.
FAQ
SSR prejudica a performance do servidor em picos de tráfego?
Sim. Cada request executa renderização no servidor. Se a página é complexa e o tráfego é alto, a CPU satura. Mitigações: cache de página inteira (CDN com Cache-Control), ISR para conteúdo semi-estático, e streaming SSR para liberar o response antes de toda a página estar pronta.
Posso misturar SSR e CSR na mesma página?
É exatamente o que o Next.js App Router faz por padrão. Server Components renderizam no servidor, Client Components renderizam no navegador. A fronteira é definida pelo "use client". Cada componente escolhe onde executa.
CSR funciona para SEO se eu usar pre-rendering?
Depende da implementação. Serviços como Prerender.io geram HTML estático para crawlers, mas adicionam complexidade de infra e latência. Se SEO é requisito de negócio, SSR ou SSG são soluções mais simples e confiáveis que pre-rendering sobre CSR.
Qual o impacto de SSR no Time to Interactive (TTI)?
SSR melhora o First Contentful Paint (o usuário vê conteúdo antes), mas pode piorar o TTI se a hydration for pesada. O HTML está visível, os botões existem, mas não respondem a cliques até o JavaScript hidratar. Reduza a superfície de Client Components e use Suspense para hidratar em chunks.
Next.js Pages Router ou App Router para SSR?
App Router. O Pages Router funciona, mas Server Components só existem no App Router. A granularidade de controle (Server Component por padrão, Client Component explícito) é superior ao modelo getServerSideProps do Pages Router, que força a decisão no nível de página inteira.

Escrito por
Marcos Soares
Fullstack Developer · CEO da Agência Poti
Fullstack Developer e CEO da Agência Poti. Mais de 20 anos construindo arquiteturas cloud-native com React, Next.js e sistemas distribuídos. Parceiro comercial do estúdio iellou design. Fundador do Vivo de Código.
Comentários
Participe da discussão
Seja o primeiro a comentar!
Continue Aprofundando
Conteúdo técnico toda semana
Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.
Sem spam. Cancele a qualquer momento com 1 clique.


