Capítulo 19: Performance Extrema com Deferrable Views (@defer) e Otimizações

Especialização em Front-End com Angular • Angular 18+ • Signals, Standalone Architecture, RxJS, NgRx e SSR


🗺️ Mapa Conceitual dos Estados das Deferrable Views (@defer)

stateDiagram-v2
    [*] --> Placeholder: Renderiza @placeholder inicial (HTML leve)
    Placeholder --> TriggerAtivado: Evento disparado (on viewport / on interaction / on idle)
    TriggerAtivado --> Loading: Inicia download assíncrono do Chunk JS (@loading)
    Loading --> Pronto: Chunk baixado e compilado com sucesso
    Loading --> Erro: Falha de conexão ou timeout de rede (@error)
    Pronto --> [*]: Componente interativo montado no DOM
    Erro --> [*]: Feedback de erro para o usuário com opção de retry

🏛️ 1. A Revolução do @defer: Code Splitting no Nível do Template

Antes do Angular 17/18, a única forma de dividir o código de uma aplicação em múltiplos arquivos JavaScript (Code Splitting) era através do roteador (loadComponent nas rotas). Isso significava que, dentro de uma mesma página complexa, componentes pesados (como gráficos, editores WYSIWYG, mapas interativos e modais) eram todos embutidos no mesmo bundle principal da página, inflando o tempo de carregamento inicial.

Com as Deferrable Views (@defer):

  • Qualquer componente Standalone pode ser empacotado em um arquivo .js separado diretamente no HTML.
  • O download do código só ocorre quando uma condição ou gatilho específico for atendido (ex: quando o usuário rolar a página até o elemento ou passar o mouse em um botão).
  • Reduz o bundle JavaScript inicial em até 60%, acelerando expressivamente as métricas do Core Web Vitals.

⚙️ 2. Anatomia Completa de um Bloco @defer

Um bloco @defer completo é composto por 4 blocos complementares:

@defer (on viewport; prefetch on idle) {
  <!-- CONTEÚDO PRINCIPAL: Baixado apenas quando entrar na tela (viewport) -->
  <!-- O prefetch baixa o JS silenciosamente antes quando a thread estiver livre (idle) -->
  <app-grafico-analitico [dados]="metricasAnuais()" />
} @placeholder (minimum 500ms) {
  <!-- PLACEHOLDER: Renderizado imediatamente sem baixar nenhum JS -->
  <div class="skeleton-box">
    <p>Aguardando rolagem para carregar gráfico...</p>
  </div>
} @loading (after 100ms; minimum 300ms) {
  <!-- LOADING: Exibido durante o download da rede para evitar pisca-pisca rápido -->
  <div class="spinner-container">
    <span class="spinner"></span> Carregando pacote de gráficos...
  </div>
} @error {
  <!-- ERROR: Exibido caso o download do chunk falhe (ex: queda de conexão) -->
  <div class="alerta-erro">
    <p>Não foi possível carregar o gráfico interativo.</p>
    <button (click)="tentarNovamente()">Tentar Novamente</button>
  </div>
}

🎯 3. Gatilhos de Carregamento (Triggers)

Você pode acionar o carregamento baseado em eventos do navegador ou condições lógicas declarativas:

Gatilho Descrição do Comportamento Exemplo de Sintaxe
on idle (Padrão) Baixa o código assim que a thread principal do navegador estiver livre. @defer (on idle)
on viewport Baixa o código quando o elemento entra na área visível da tela do usuário. @defer (on viewport) ou @defer (on viewport(refElemento))
on interaction Baixa quando o usuário clica ou foca no placeholder. @defer (on interaction)
on hover Baixa quando o cursor do mouse passa por cima do placeholder. @defer (on hover)
on immediate Inicia o download imediatamente após a página carregar, sem esperar idle. @defer (on immediate)
on timer(ms) Inicia o download após um atraso estipulado em milissegundos. @defer (on timer(2000ms))
when condicao Condição booleana de um Signal ou variável do componente. @defer (when usuarioClicouNoBotao())

3.1 Padrão Ouro: Combinação com prefetch

O uso mais poderoso do @defer é separar o momento do download do código do momento da renderização:

<!-- Baixa o JavaScript assim que o mouse passar por perto (hover) -->
<!-- Renderiza de fato apenas quando o usuário clicar (interaction) -->
@defer (on interaction; prefetch on hover) {
  <app-modal-comentarios [post]="postAtual()" />
} @placeholder {
  <button>Ver Comentários (Clique aqui)</button>
}

🖼️ 4. Otimização de Imagens com NgOptimizedImage

Imagens desproporcionais e não otimizadas são a maior causa de notas ruins em LCP (Largest Contentful Paint) e CLS (Cumulative Layout Shift). A diretiva NgOptimizedImage (ngSrc) do pacote @angular/common resolve isso automaticamente:

import { Component } from '@angular/core';
import { NgOptimizedImage } from '@angular/common';

@Component({
  selector: 'app-banner-hero',
  standalone: true,
  imports: [NgOptimizedImage],
  template: `
    <!-- Imagem Hero crítica: priority força download imediato no topo da fila -->
    <img 
      ngSrc="/assets/imagens/banner-principal.webp" 
      width="1200" 
      height="600" 
      priority
      alt="Banner Promocional" 
    />

    <!-- Imagem comum: lazy loading automático e prevenção de CLS via aspect ratio -->
    <img 
      ngSrc="/assets/imagens/produto-thumb.webp" 
      width="300" 
      height="200" 
      alt="Miniatura do Produto" 
    />
  `
})
export class BannerHeroComponent {}

⚖️ 5. Comparativo: @defer vs loadComponent do Roteador

Dimensão @defer (Template Level) loadComponent (Route Level)
Granularidade Sub-componente individual dentro de uma view Rota / Página inteira
Acionador Viewport, hover, clique, idle, timer, booleano Mudança de URL no navegador
Feedback de UI Suporte integrado a @placeholder, @loading e @error Requer Resolvers ou componentes intermediários
Configuração 100% declarativo no template HTML Configuração no array routes

⚠️ 6. Armadilhas Comuns e Anti-Patterns

  1. Esquecer de Adicionar Componente aos imports:
    • O componente dentro de @defer DEVE ser Standalone e precisa estar listado no array imports do componente pai, caso contrário o compilador não saberá como criar o chunk preguiçoso.
  2. Pisca-Pisca de Loading (Flickering):
    • Se o chunk baixar em 20ms, exibir um spinner por 20ms causa um efeito visual incômodo. Utilize sempre parâmetros de guarda: @loading (after 150ms; minimum 300ms).

🛡️ 7. Boas Práticas e Core Web Vitals

  • LCP: Marque sempre a maior imagem visível acima da dobra (above the fold) com o atributo priority usando NgOptimizedImage.
  • CLS: Sempre defina atributos width e height em imagens ou utilize fill com containeres de aspect-ratio fixo para reservar o espaço físico no DOM antes do download.

📋 8. Cheat-Sheet: Resumo Executivo

@defer (on viewport; prefetch on idle) {
  <app-componente-pesado />
} @placeholder (minimum 300ms) {
  <div class="skeleton"></div>
} @loading (after 100ms) {
  <p>Carregando...</p>
}