Voltar para Artigos
Front-end10 min de leitura

Next.js App Router: Server Components, Streaming e Estratégias de Cache

React Server Components vs Client Components, fetch com cache e revalidação (force-cache, no-store, revalidate), Suspense para streaming progressivo, generateStaticParams para SSG e quando usar cada estratégia.

12 de agosto de 2026

O App Router do Next.js introduziu uma mudança arquitetural profunda: por padrão, todos os componentes são React Server Components (RSC). Eles rodam exclusivamente no servidor — sem enviar JavaScript para o cliente, sem hidratação, com acesso direto a bancos de dados e secrets. Apenas os componentes que precisam de interatividade (useState, useEffect, event handlers) recebem a diretiva 'use client'.

Essa distinção impacta performance, SEO e segurança de forma fundamental. Este artigo cobre as estratégias de renderização e cache do App Router — leia o arquivo em node_modules/next/dist/docs/ para as convenções atuais antes de escrever código.

Server Components vs Client Components

  • Server Component (padrão) — roda no servidor em tempo de requisição ou build. Pode ser async, acessar banco de dados direto, usar secrets, ler o filesystem. Não pode usar hooks, event handlers ou APIs de browser.
  • Client Component (`'use client'`) — hidratado no browser. Pode usar hooks e event handlers. Recebe dados via props do Server Component pai. Bundle JavaScript enviado ao cliente.
  • Regra de composição: Server Components podem importar Client Components. Client Components não podem importar Server Components (apenas receber como children via props).

O Server Component assíncrono quebra um paradigma: antes, toda página precisava de getServerSideProps ou getStaticProps separados do componente. Com RSC, o próprio componente é async e faz a busca de dados diretamente — sem funções auxiliares, sem API route intermediária. O Suspense com fallback é o que permite o streaming progressivo: o HTML do shell da página é enviado imediatamente, e as partes dentro do Suspense são transmitidas para o browser conforme ficam prontas no servidor.

page.tsx (Server Component)
import { Suspense } from 'react';
import { ProductList } from './ProductList';
import { ProductListSkeleton } from './ProductListSkeleton';
import { db } from '../../lib/db';

// generateMetadata: SEO dinâmico — roda no servidor
export async function generateMetadata() {
  return {
    title: 'Produtos | Loja',
    description: 'Explore nossa linha completa de produtos.',
  };
}

// Page: Server Component assíncrono
export default async function ProductsPage({
  searchParams,
}: {
  searchParams: { page?: string; category?: string };
}) {
  // Acesso direto ao banco — sem API route intermediária
  // A query roda no servidor — o cliente nunca vê a connection string
  const categories = await db.query.categories.findMany({
    orderBy: (c, { asc }) => asc(c.name),
  });

  return (
    <main>
      <h1>Nossos Produtos</h1>

      <CategoryFilter categories={categories} /> {/* Client Component */}

      {/* Suspense: streaming progressivo
          A página carrega instantaneamente com o skeleton
          enquanto a lista de produtos (potencialmente mais lenta) ainda carrega */}
      <Suspense fallback={<ProductListSkeleton />}>
        <ProductList
          page={Number(searchParams.page ?? 1)}
          category={searchParams.category}
        />
      </Suspense>
    </main>
  );
}

O fetch dentro de Server Components tem cache integrado com semântica configurável por chamada. Isso é radicalmente diferente do comportamento padrão do fetch no browser: no App Router, cada chamada fetch decide sua própria estratégia de cache — o mesmo componente pode ter uma parte dos dados com cache de 1 hora e outra parte sem cache algum.

ProductList.tsx (Server Component com fetch cacheado)
interface ProductListProps {
  page: number;
  category?: string;
}

async function getProducts(page: number, category?: string) {
  // fetch no App Router tem cache integrado
  const res = await fetch(
    `${process.env.API_URL}/products?page=${page}${category ? `&category=${category}` : ''}`,
    {
      // Estratégias de cache:

      // force-cache (padrão): reutiliza o cache indefinidamente
      // Igual ao SSG — use para dados que raramente mudam
      // next: { cache: 'force-cache' }

      // no-store: sem cache — busca sempre frescos
      // Use para dados altamente dinâmicos (estoque em tempo real)
      // cache: 'no-store'

      // revalidate: cache por tempo (ISR — Incremental Static Regeneration)
      // Reutiliza cache por N segundos, depois regenera em background
      next: { revalidate: 60 }, // Cache por 60 segundos
    }
  );

  if (!res.ok) throw new Error(`API error: ${res.status}`);
  return res.json() as Promise<{ data: Array<Product>; meta: PaginationMeta }>;
}

export async function ProductList({ page, category }: ProductListProps) {
  const { data: products, meta } = await getProducts(page, category);

  return (
    <div>
      <ul className="grid grid-cols-3 gap-6">
        {products.map((product) => (
          <ProductCard key={product.id} product={product} />
        ))}
      </ul>
      <Pagination meta={meta} /> {/* Client Component para interatividade */}
    </div>
  );
}

O generateStaticParams é o equivalente moderno do getStaticPaths: ele instrui o Next.js a gerar páginas estáticas durante o build para as rotas mais acessadas. Páginas geradas estaticamente têm latência de zero do servidor — são servidas diretamente da CDN. O dynamicParams = true (padrão) garante que rotas não pré-geradas sejam renderizadas sob demanda (ISR) em vez de retornar 404.

page.tsx (SSG com generateStaticParams)
// generateStaticParams: gera páginas estáticas em build time
// para as rotas mais acessadas — zero latência de servidor
export async function generateStaticParams() {
  const products = await fetch(`${process.env.API_URL}/products?limit=100`)
    .then((r) => r.json());

  // Gera páginas estáticas para os 100 produtos mais populares
  return products.data.map((product: Product) => ({
    slug: product.slug,
  }));
}

// Produtos não listados em generateStaticParams são gerados
// sob demanda (ISR) ou retornam 404
export const dynamicParams = true; // Permite geração on-demand

export default async function ProductDetailPage({
  params,
}: {
  params: { slug: string };
}) {
  const product = await fetch(
    `${process.env.API_URL}/products/${params.slug}`,
    { next: { revalidate: 300 } } // Revalida a cada 5 minutos
  ).then((r) => {
    if (!r.ok) throw new Error('Not found');
    return r.json();
  });

  return (
    <main>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </main>
  );
}
'use client': Componente interativo
'use client';

import { useState } from 'react';

interface CategoryFilterProps {
  categories: Array<Category>;
}

// Client Component: precisa de estado local e URL
export function CategoryFilter({ categories }: CategoryFilterProps) {
  const [selected, setSelected] = useState<string | null>(null);
  const router = useRouter();
  const searchParams = useSearchParams();

  function handleSelect(categoryId: string) {
    setSelected(categoryId);
    const params = new URLSearchParams(searchParams.toString());
    params.set('category', categoryId);
    router.push(`?${params.toString()}`);
  }

  return (
    <div className="flex gap-2">
      {categories.map((cat) => (
        <button
          key={cat.id}
          onClick={() => handleSelect(cat.id)}
          className={selected === cat.id ? 'bg-indigo-600 text-white' : 'bg-gray-100'}
        >
          {cat.name}
        </button>
      ))}
    </div>
  );
}

Quando usar cada estratégia: force-cache para landing pages, páginas de documentação e conteúdo editorial que muda raramente. revalidate: N (ISR) para listagens de produtos, preços — dados que mudam às vezes. no-store para dados em tempo real (estoque, cotações). 'use client' apenas para componentes que precisam de interatividade — não para toda a página.

Conclusão

O App Router do Next.js muda o modelo mental de 'página que busca dados' para 'componentes que são dados'. Server Components trazem dados do banco direto para a renderização sem JavaScript extra no cliente. O Suspense com streaming entrega o HTML principal instantaneamente enquanto partes mais lentas carregam em paralelo. A estratégia de cache por componente (force-cache, revalidate, no-store) dá controle granular entre performance e atualidade dos dados.