Voltar ao Blog
Angular Development15 de julho de 20268 min de leitura

Angular Lazy Loading: Apps Mais Rápidos Com Menos Esforço

DR

Doug Ribeiro

8 min de leitura

A primeira coisa que faço em qualquer base de código Angular é rodar uma análise de bundle. O padrão que eu procuro é familiar: um usuário abrindo a primeira tela baixa código de features e etapas de fluxo que talvez nunca alcance. Lazy loading é a correção, e pode entrar do jeito mais sem graça possível: uma rota de feature por vez, através de pull requests revisados normais. Sem heroísmo, sem refatoração big-bang, e ainda assim a vitória de performance mais barata que conheço. Os exemplos de código abaixo usam um domínio de solicitação de empréstimo apenas como ilustração; nenhum vem do codebase de um empregador.

Por Que o Setup Padrão do Angular Te Prejudica

Match da rota loadComponent() Requisição de rede Inicialização do componente Renderização

O build padrão do Angular coloca tudo em um único chunk principal. Todo componente que é importado em qualquer lugar da árvore de módulos carregados eagerly acaba nesse chunk. Um módulo de relatórios importado no app.module.ts ou app.config.ts arrasta consigo toda biblioteca de gráficos, todo componente de tabela, e todo service que os relatórios precisam. O usuário requisitando a tela de login paga por tudo isso.

Lazy loading move código específico de rota para chunks separados que são buscados sob demanda. Um usuário que nunca navega para relatórios nunca baixa o código de relatórios. O First Contentful Paint melhora. O Time to Interactive melhora. Os scores do Lighthouse melhoram. E a experiência real do usuário melhora nas conexões que importam, que nunca são as conexões de fibra em que os desenvolvedores testam.

loadComponent para Componentes de Rota Única

Com standalone components, são três linhas de código por rota.

typescript
// app.routes.ts
export const routes: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./features/dashboard/dashboard.component')
        .then(m => m.DashboardComponent)
  },
  {
    path: 'applications',
    canActivate: [authGuard],
    loadComponent: () =>
      import('./features/applications/applications.component')
        .then(m => m.ApplicationsComponent)
  },
  {
    path: 'reports',
    canActivate: [authGuard, roleGuard('analyst')],
    loadComponent: () =>
      import('./features/reports/reports.component')
        .then(m => m.ReportsComponent)
  }
];

O import dinâmico é a fronteira. O compilador do Angular cria um chunk separado para tudo que é alcançável a partir daquele import. Se ReportsComponent importa uma biblioteca de gráficos de 400KB e um renderizador de PDF de 200KB, eles ficam fora do bundle principal.

loadChildren para Árvores de Rotas de Feature

Para features com múltiplas rotas aninhadas, loadChildren é a escolha certa. Ele adia um arquivo de rotas inteiro no próprio chunk, junto com tudo que esse arquivo importa estaticamente. Rotas filhas ainda podem usar loadComponent para dividir mais, como abaixo.

typescript
// app.routes.ts
export const routes: Routes = [
  {
    path: 'lending',
    canActivate: [authGuard],
    loadChildren: () =>
      import('./features/lending/lending.routes')
        .then(m => m.LENDING_ROUTES)
  }
];

// features/lending/lending.routes.ts
export const LENDING_ROUTES: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./application-list/application-list.component')
        .then(m => m.ApplicationListComponent)
  },
  {
    path: ':id',
    loadComponent: () =>
      import('./application-detail/application-detail.component')
        .then(m => m.ApplicationDetailComponent)
  },
  {
    path: ':id/documents',
    loadComponent: () =>
      import('./document-upload/document-upload.component')
        .then(m => m.DocumentUploadComponent)
  }
];

Preloading: Lazy Sem o Atraso na Navegação

A desvantagem do lazy loading é que a primeira navegação para uma rota lazy dispara uma busca na rede, o que causa um breve atraso. Estratégias de preloading fecham essa lacuna carregando chunks em background depois que o app inicializa, sem bloquear a primeira renderização.

typescript
// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withPreloading(PreloadAllModules))
  ]
};

PreloadAllModules funciona para a maioria dos apps. Para apps maiores onde largura de banda importa, um preloader seletivo customizado carrega apenas rotas de alta prioridade.

typescript
@Injectable({ providedIn: 'root' })
export class PriorityPreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<unknown>): Observable<unknown> {
    return route.data?.['preload'] === true ? load() : of(null);
  }
}

// Mark routes that should preload
{
  path: 'applications',
  data: { preload: true },
  loadChildren: () =>
    import('./features/applications/applications.routes')
      .then(m => m.APPLICATIONS_ROUTES)
}

Como Auditar Seu Bundle Atual

  • Execute ng build --stats-json para gerar um arquivo stats.json (com o builder application padrão ele é um metafile do esbuild que o analisador de bundle do esbuild consegue ler)
  • Faça o build com --source-map e depois execute npx source-map-explorer dist/your-app/browser/main-*.js para visualizar o que está dentro
  • Procure qualquer biblioteca acima de 50KB que não seja necessária na tela inicial
  • Procure componentes de feature que são importados diretamente no app.config.ts ou em módulos carregados eagerly
  • Identifique rotas que a maioria dos usuários nunca visita. Essas são os primeiros candidatos

Erros Que Anulam o Lazy Loading

  • Importar um componente lazy diretamente em qualquer lugar da árvore carregada eagerly: o import anula a fronteira lazy
  • providedIn: 'any' em um service usado por uma rota lazy: isso é comportamento documentado e não um defeito, e mesmo assim pega as pessoas. Você ganha uma instância por injector lazy, não um singleton. Use 'root' quando o estado precisa ser compartilhado entre features
  • Bibliotecas third-party importadas em módulos compartilhados que são carregados eagerly: mova-as para a feature que precisa delas
  • Pular o preloading e aceitar que toda primeira navegação para uma rota lazy tem um atraso visível

Lazy loading não é uma micro-otimização. Frequentemente é o caminho mais rápido de um app lento para um rápido, medido em horas de trabalho e segundos de melhoria.

Execute a análise de bundle primeiro. Os números vão te dizer exatamente por onde começar. Em todo app que auditei, duas ou três rotas são responsáveis pela maior parte do peso desnecessário do bundle principal. Corrija essas e meça novamente. A relação esforço-impacto do lazy loading supera toda outra técnica de performance que eu conheço.

Referências

AngularPerformanceTypeScriptFrontend