SSR, hydration e @defer sem chute

Estratégia de renderização mora em configuração de rota, medição e restrições de DOM, não em uma decisão genérica de SSR. Hydration ficou stable no Angular v17 (novembro/2023), @defer na v18 (maio/2024) e hydration incremental na v20 (maio/2025). O stack é production-ready; o que diferencia é o desenho.

Transforme renderização em decisão por rota

Comece um nível abaixo de "o app deve usar SSR?" Pergunte quais rotas precisam de qual modo de renderização. Pricing, artigo de blog, dashboard logado e checkout não têm as mesmas restrições.

A configuração de server routes do Angular dá ao time um lugar para registrar essa decisão no código. Isso importa porque estratégia de renderização precisa sobreviver ao code review, não sumir em notas de reunião.

Em rotas parametrizadas com prerender, mantenha chamadas de inject() antes de qualquer await dentro de getPrerenderParams. Isso mantém o exemplo alinhado com a regra de contexto de injeção síncrono do Angular.

Tipo de rotaModo a testarEvidência exigida
Rota pública de marketingPrerenderSEO, conteúdo estável e pouca personalização por request
Blog ou docsPrerender com paramsSlugs conhecidos no build e custo de build aceitável
Dashboard logadoClient renderingDados privados, UI browser-only pesada e SEO irrelevante
Checkout ou detalhe de produtoServer renderingLCP e conversão melhoram sem vazar dados de usuário
Fallback desconhecidoServer rendering ou política explícita de 404Comportamento claro para rotas não geradas no build
Render modes por rotats
// app.routes.server.ts
import { inject } from '@angular/core';
import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  {
    path: '',
    renderMode: RenderMode.Prerender,
  },
  {
    path: 'pricing',
    renderMode: RenderMode.Prerender,
  },
  {
    path: 'blog/:slug',
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      const posts = inject(PostCatalog);
      const slugs = await posts.publicSlugs();
      return slugs.map((slug) => ({ slug }));
    },
  },
  {
    path: 'app/**',
    renderMode: RenderMode.Client,
  },
  {
    path: 'checkout',
    renderMode: RenderMode.Server,
  },
  {
    path: '**',
    renderMode: RenderMode.Server,
  },
];

Hydration é contrato, não interruptor

Hydration só funciona quando o DOM do servidor e o DOM do cliente concordam. DOM manual, HTML inválido, rewrites de CDN e pressupostos de browser não são detalhe pequeno. São motivos para uma página renderizada no servidor falhar quando o cliente tenta reaproveitar o HTML existente.

Habilite hydration com event replay em rotas públicas onde interação cedo importa, e deixe a política de transfer cache explícita. Endpoints específicos de usuário não deveriam virar dados reaproveitáveis no HTML inicial por acidente.

Em setups SSR customizados, o provider de hydration também precisa entrar na configuração de bootstrap do servidor. Caso contrário, cliente e servidor não estão seguindo o mesmo contrato de renderização.

Hydration e transfer cachets
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import {
  provideClientHydration,
  withEventReplay,
  withHttpTransferCacheOptions,
} from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [
    provideClientHydration(
      withEventReplay(),
      withHttpTransferCacheOptions({
        filter: (req) => !req.url.includes('/api/profile'),
        includeRequestsWithAuthHeaders: false,
      })
    ),
  ],
};

`@defer` deve tirar JavaScript real do caminho inicial

@defer não é estado de loading decorativo. Ele merece entrar quando uma dependência standalone é pesada o suficiente para que adiar seu carregamento melhore o caminho inicial ou o caminho de interação.

Revise o diff com três perguntas: qual componente sai do bundle inicial, qual placeholder evita layout shift e o que acontece se o código deferido falhar ao carregar?

Os blocos de placeholder, loading e error são carregados de forma eager, então mantenha esses blocos baratos. E em SSR ou SSG, um @defer normal renderiza o placeholder no servidor; se o conteúdo principal precisa renderizar no servidor, a conversa passa para incremental hydration.

Defer de um chart pesado de relatóriohtml
<section
  class="report-route"
  aria-live="polite"
  aria-atomic="true"
>
  <app-report-summary [summary]="summary()" />

  @defer (on viewport; prefetch on idle) {
    <app-revenue-chart [series]="series()" />
  } @placeholder (minimum 600ms) {
    <app-chart-skeleton aria-label="Chart loading" />
  } @loading (after 200ms; minimum 600ms) {
    <app-chart-skeleton aria-label="Chart loading" />
  } @error {
    <app-inline-error message="Could not load the chart" />
  }
</section>

Incremental hydration é para ilhas com motivo

Incremental hydration mantém conteúdo renderizado no servidor visível enquanto atrasa a hidratação de seções escolhidas. Isso faz sentido para seções caras que não precisam estar interativas imediatamente.

Não faça disso a primeira tarefa de SSR em um app maduro. Primeiro prove que a hydration normal está limpa, depois teste uma rota onde uma ilha cara tem ganho visível. O Angular habilita event replay automaticamente com incremental hydration, então o time também precisa testar cliques e teclado antes da hidratação.

Habilitar incremental hydrationts
// main.ts
bootstrapApplication(AppComponent, {
  providers: [
    provideClientHydration(withIncrementalHydration()),
  ],
});
Hidratar o chart quando entrar no viewporthtml
@defer (hydrate on viewport) {
  <app-revenue-chart [series]="series()" />
} @placeholder {
  <app-chart-skeleton aria-label="Chart loading" />
}

Mova DOM browser-only para fora da renderização

SSR expõe código que assume window, document, medição de layout ou biblioteca de chart disponível durante renderização. Evite transformar isso em branches de isPlatformBrowser espalhados pelo template.

Para trabalho de DOM que só faz sentido no browser, use hooks como afterNextRender. O HTML renderizado continua consistente, e o side effect browser-only roda depois que o Angular renderizou no browser.

Medição browser-only depois da renderizaçãots
export class ChartHostComponent {
  private readonly chartHost = viewChild.required<ElementRef>('chartHost');

  constructor() {
    afterNextRender(() => {
      const width = this.chartHost().nativeElement.clientWidth;
      this.resizeChart(width);
    });
  }

  private resizeChart(width: number) {
    // browser-only chart API
  }
}

O spike deve gerar números e exceções

Mudanças de renderização podem soar maiores do que a evidência. Mantenha o spike estreito: medir o estado atual, mudar uma rota ou um componente pesado, e registrar o que melhorou, o que quebrou e o que precisou ser pulado.

ngSkipHydration pode ser uma exceção tática para um componente legado que muta DOM, mas precisa ter nome do componente, motivo, responsável e plano de remoção. Se a lista de exceções cresce, hydration está mostrando algo sobre a codebase.

HipóteseEvidência mínimaDecisão
Prerender melhora conteúdo públicoHTML existe no build, metadata de SEO está presente e tempo de build é aceitávelUsar prerender para rotas públicas estáveis
SSR melhora uma rota críticaLCP melhora e logs de hydration ficam limposUsar server rendering nessa rota
@defer ajuda um componente pesadoBundle inicial diminui e placeholder não causa CLSManter defer e documentar o gatilho
Incremental hydration ajuda uma rotaEventos iniciais são reproduzidos e ilha não crítica hidrata depoisManter apenas nessa rota medida
DOM legado bloqueia hydrationMismatch reproduzido e owner identificadongSkipHydration temporário com plano de remoção
Comandos para baseline de renderizaçãobash
rm -rf dist .angular/cache
time npx ng build --configuration production --stats-json
npx ng test --no-watch
npx lighthouse https://staging.example.com/critical-route --only-categories=performance

Artefato para reaproveitar

Checklist de review para estratégia de renderização

  • Classifique cada rota prioritária como CSR, prerender ou SSR antes de mexer na configuração.
  • Ao usar getPrerenderParams, mantenha inject() antes de qualquer await.
  • Habilite hydration só com auditoria de DOM: HTML válido, sem mismatch servidor/cliente e sem mutação direta descontrolada.
  • Filtre transfer cache para endpoints sensíveis ou específicos de usuário.
  • Use @defer apenas quando uma dependência standalone pesada sai do caminho inicial, o placeholder continua barato e o CLS fica estável.
  • Trate incremental hydration como experimento medido por rota, não como default do app inteiro.
  • Documente todo ngSkipHydration com responsável, motivo, risco e plano de remoção.

Fontes consultadas

Modern Angular Playbook

Este artigo é um play.

O Playbook do Angular Moderno reúne o diagnóstico, a matriz de adoção, onze jogadas e o plano de 30 dias. Grátis, em inglês e português.

Você recebe os dois PDFs por email, pela lista do Dojo IA.

Abrir playbook →

André Ramosdisponível para vagas remotas, UTC−3Entre em contato →

Leia também

Erro · 8 min

NG0500 Hydration Node Mismatch: o conserto de verdade não é ngSkipHydration

O que o NG0500 Hydration Node Mismatch quer dizer, a lista curta de causas reais (DOM manipulado direto, HTML inválido que o browser normaliza, biblioteca de DOM de terceiro, HTML alterado no caminho), por que o ngSkipHydration é torniquete, e o fix de cada caso.

Ler artigo →

Guia · 10 min

O código que quebra quando Angular fica zoneless

Um guia técnico para revisar timers, subscriptions, forms, callbacks de terceiros e testes antes de um spike zoneless no Angular.

Ler artigo →

Guia · 9 min

Estado HTTP sem transformar tudo em Signals

Um guia técnico para fronteiras de estado HTTP no Angular com HttpClient, RxJS, toSignal e experimentos cautelosos com httpResource.

Ler artigo →