Capítulo 19: Performance Extrema com Deferrable Views (@defer) e Otimizações
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
.jsseparado 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
- Esquecer de Adicionar Componente aos
imports:- O componente dentro de
@deferDEVE ser Standalone e precisa estar listado no arrayimportsdo componente pai, caso contrário o compilador não saberá como criar o chunk preguiçoso.
- O componente dentro de
- 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).
- Se o chunk baixar em 20ms, exibir um spinner por 20ms causa um efeito visual incômodo. Utilize sempre parâmetros de guarda:
🛡️ 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
priorityusandoNgOptimizedImage. - CLS: Sempre defina atributos
widtheheightem imagens ou utilizefillcom 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>
}