Você já se perguntou por que sua aplicação Angular demora para carregar, mesmo sendo uma SPA? Ou talvez tenha enfrentado problemas de performance com bundles enormes sendo carregados de uma vez?
Hoje, vou compartilhar Roteamento Avançado no Angular 17+ - as técnicas e estratégias que transformam aplicações lentas em experiências ultra-rápidas. Ao final deste artigo, você dominará lazy loading, estratégias de preload e implementações que seus usuários vão adorar.
O Que é Roteamento Moderno no Angular?
O roteamento no Angular 17+ não é apenas sobre navegar entre páginas - é sobre criar experiências fluidas, carregamento inteligente de recursos e otimização automática de performance. Com as novas funcionalidades standalone components e melhorias no router, temos ferramentas poderosas para construir aplicações mais eficientes.
Por Que Isso Importa
Antes de mergulharmos na implementação, vamos entender o problema que estamos resolvendo:
// ❌ Sem lazy loading - todos os módulos carregados de uma vez
const routes: Routes = [
{ path: 'dashboard', component: DashboardComponent },
{ path: 'users', component: UsersComponent },
{ path: 'products', component: ProductsComponent },
{ path: 'analytics', component: AnalyticsComponent },
// Bundle inicial gigante = 2MB+
];
// ✅ Com lazy loading - carregamento sob demanda
const routes: Routes = [
{
path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard.component')
},
{
path: 'users',
loadChildren: () => import('./users/users.routes')
},
// Bundle inicial = 300KB, módulos carregados conforme necessário
];Esta transformação reduz o tempo de carregamento inicial de segundos para milissegundos, criando uma experiência muito mais responsiva para seus usuários.
Quando Usar Roteamento Avançado?
Bons casos de uso:
Aplicações com múltiplas features/módulos distintos
Dashboards complexos com diferentes seções
E-commerce com catálogo, checkout, admin
Sistemas ERP/CRM com módulos independentes
Quando NÃO usar lazy loading:
Aplicações muito pequenas (< 5 rotas)
Features que sempre são acessadas juntas
Quando a latência de rede é mais crítica que o bundle size
Configuração: Preparando Seu Roteamento Moderno
Vamos construir isso passo a passo. Mostrarei como cada peça funciona e por que cada decisão importa.
Passo 1: Configuração Base - Standalone Components
Primeiro, precisamos configurar a base com standalone components (recomendado no Angular 17+):
Opção 1: Bootstrap com Standalone (Recomendado)
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
// outros providers...
]
});Opção 2: Módulo Tradicional (Legado)
// app.module.ts
import { RouterModule } from '@angular/router';
@NgModule({
imports: [
RouterModule.forRoot(routes, {
enableTracing: false, // apenas para debug
preloadingStrategy: PreloadAllModules
})
],
// ...
})
export class AppModule { }Como configurar rotas standalone:
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
// Rota básica standalone
{
path: '',
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
},
// Lazy loading de feature completa
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.routes').then(m => m.dashboardRoutes)
},
// Redirecionamento e wildcard
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: '**', loadComponent: () => import('./not-found/not-found.component') }
];Por que essa configuração funciona tão bem:
Inicialização mais rápida: Componentes standalone reduzem overhead de módulos
Tree-shaking melhorado: Apenas código usado é incluído no bundle
Desenvolvimento mais simples: Menos boilerplate, foco no que importa
Passo 2: Implementando Lazy Loading Inteligente
Agora vamos implementar lazy loading de forma estratégica, explicando cada decisão:
2.1: Lazy Loading de Componentes Simples
Primeiro, vamos criar a estrutura básica para componentes standalone:
// feature/product/product.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-product',
standalone: true,
imports: [CommonModule, RouterModule],
template: `
<div class="product-container">
<h2>Catálogo de Produtos</h2>
<router-outlet></router-outlet>
</div>
`
})
export class ProductComponent { }2.2: Configuração de Rotas com Lazy Loading
Agora vamos implementar lazy loading com diferentes estratégias:
// app.routes.ts
import { Routes } from '@angular/router';
import { inject } from '@angular/core';
import { AuthGuard } from './guards/auth.guard';
export const routes: Routes = [
// Componente standalone com lazy loading
{
path: 'profile',
canActivate: [() => inject(AuthGuard).canActivate()],
loadComponent: () => import('./profile/profile.component')
.then(m => m.ProfileComponent)
},
// Feature module completo com subrotas
{
path: 'products',
loadChildren: () => import('./products/products.routes')
.then(m => m.productRoutes),
data: { preload: true } // Marca para preload
},
// Loading condicional baseado em permissões
{
path: 'admin',
canMatch: [() => inject(AuthGuard).hasAdminRole()],
loadChildren: () => import('./admin/admin.routes')
}
];Por que essa implementação?
Segurança: Guards verificam permissões antes do loading
Performance: Cada feature carrega apenas quando necessária
Flexibilidade: Diferentes estratégias para diferentes necessidades
2.3: Criando Rotas de Feature
Vamos criar um sistema de rotas hierárquico para features complexas:
// products/products.routes.ts
import { Routes } from '@angular/router';
export const productRoutes: Routes = [
{
path: '',
loadComponent: () => import('./product-layout.component')
.then(m => m.ProductLayoutComponent),
children: [
{
path: '',
loadComponent: () => import('./product-list/product-list.component')
},
{
path: 'category/:id',
loadComponent: () => import('./product-category/product-category.component'),
data: { preload: true }
},
{
path: 'details/:id',
loadComponent: () => import('./product-details/product-details.component'),
resolve: {
product: () => inject(ProductService).getProduct(
inject(ActivatedRoute).snapshot.params['id']
)
}
}
]
}
];Diferenças importantes:
Rotas aninhadas: Estrutura hierárquica clara para UX consistente
Resolvers: Dados carregados antes da navegação, evitando loading states
Data binding: Metadados para controle de preload e cache
2.4: Implementação com Guards Funcionais
// guards/auth.guard.ts
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
export const authGuard = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
return router.parseUrl('/login');
};
// Usando o guard
{
path: 'dashboard',
canActivate: [authGuard],
loadChildren: () => import('./dashboard/dashboard.routes')
}Guards funcionais explicados:
Mais limpos: Sem classes, apenas funções puras
Melhor testabilidade: Fácil de mockar e testar
Performance: Menos overhead de instanciação
Passo 3: Estratégias de Preloading Inteligentes
// strategies/smart-preload.strategy.ts
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of, timer } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
@Injectable()
export class SmartPreloadStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
// Preload apenas se marcado nos data
if (route.data?.['preload']) {
// Aguarda 2 segundos após carregamento inicial
return timer(2000).pipe(
mergeMap(() => load())
);
}
return of(null);
}
}
// Configuração no provider
provideRouter(routes, withPreloading(SmartPreloadStrategy))Como todas as peças trabalham juntas: o sistema carrega a rota inicial instantaneamente, então identifica rotas marcadas para preload e as carrega em background após um delay, garantindo que recursos críticos não sejam bloqueados.
Exemplo Complexo: E-commerce com Micro-frontend
Vamos construir algo mais realista - um e-commerce completo que demonstra uso avançado:
Entendendo o Problema
Antes de pular para o código, vamos entender o que estamos construindo:
// ❌ Abordagem ingênua - tudo carregado junto
const routes: Routes = [
{ path: 'products', component: ProductsComponent },
{ path: 'cart', component: CartComponent },
{ path: 'checkout', component: CheckoutComponent },
{ path: 'admin', component: AdminComponent },
// Bundle inicial = 3.5MB, tempo de carregamento = 8s
];
// ✅ Nossa abordagem - carregamento estratégico
const routes: Routes = [
{
path: 'products',
loadChildren: () => import('./features/catalog/catalog.routes'),
data: { preload: true } // Provavelmente será acessado
},
{
path: 'checkout',
loadChildren: () => import('./features/checkout/checkout.routes')
// Carregado apenas quando necessário
}
// Bundle inicial = 280KB, carregamento incremental
];Implementação Passo a Passo
Fase 1: Estrutura Base do E-commerce
// app.routes.ts
import { Routes } from '@angular/router';
import { authGuard } from './guards/auth.guard';
import { SmartPreloadStrategy } from './strategies/smart-preload.strategy';
export const routes: Routes = [
// Landing page - carregamento imediato
{
path: '',
loadComponent: () => import('./features/home/home.component')
},
// Catálogo - preload habilitado (alta probabilidade de acesso)
{
path: 'products',
loadChildren: () => import('./features/catalog/catalog.routes'),
data: {
preload: true,
priority: 'high'
}
},
// Carrinho - carregamento sob demanda
{
path: 'cart',
loadChildren: () => import('./features/cart/cart.routes'),
data: { preload: false }
},
// Checkout - carregamento protegido
{
path: 'checkout',
canActivate: [authGuard],
loadChildren: () => import('./features/checkout/checkout.routes'),
data: {
requiresAuth: true,
preloadOnAuth: true // Preload quando usuário autenticar
}
},
// Admin - acesso restrito
{
path: 'admin',
canMatch: [() => inject(AuthService).hasRole('admin')],
loadChildren: () => import('./features/admin/admin.routes')
}
];Quebrando isso:
Priorização inteligente: Recursos mais usados têm prioridade
Segurança por layers: Guards em diferentes níveis conforme necessidade
Preload condicional: Baseado em contexto do usuário
Fase 2: Catalog Module com Subrotas Otimizadas
// features/catalog/catalog.routes.ts
import { Routes } from '@angular/router';
import { productResolver } from './resolvers/product.resolver';
export const catalogRoutes: Routes = [
{
path: '',
loadComponent: () => import('./catalog-layout.component'),
children: [
// Lista de produtos - view principal
{
path: '',
loadComponent: () => import('./views/product-list/product-list.component'),
data: {
title: 'Produtos',
description: 'Catálogo completo de produtos'
}
},
// Categoria específica - preload ativado
{
path: 'category/:slug',
loadComponent: () => import('./views/category/category.component'),
resolve: {
category: (route: ActivatedRouteSnapshot) =>
inject(CategoryService).getBySlug(route.params['slug'])
},
data: { preload: true }
},
// Detalhes do produto - resolvers para UX
{
path: 'product/:id',
loadComponent: () => import('./views/product-details/product-details.component'),
resolve: {
product: productResolver,
recommendations: (route: ActivatedRouteSnapshot) =>
inject(RecommendationService).getFor(route.params['id'])
}
},
// Busca - componente leve
{
path: 'search',
loadComponent: () => import('./views/search/search.component'),
data: {
preload: true,
cache: true // Cache results
}
}
]
}
];Por que essa integração funciona:
Resolvers inteligentes: Dados carregados antes da navegação
Cache strategy: Evita requests desnecessários
Estrutura hierárquica: Reutilização de layout e estado
Fase 3: Implementação de Checkout Avançado
// features/checkout/checkout.routes.ts
import { Routes } from '@angular/router';
import { cartGuard } from './guards/cart.guard';
import { stepGuard } from './guards/step.guard';
export const checkoutRoutes: Routes = [
{
path: '',
canActivate: [cartGuard], // Verifica se há itens no carrinho
loadComponent: () => import('./checkout-layout.component'),
children: [
// Processo de checkout em steps
{
path: '',
redirectTo: 'shipping',
pathMatch: 'full'
},
// Step 1: Endereço de entrega
{
path: 'shipping',
loadComponent: () => import('./steps/shipping/shipping.component'),
data: { step: 1, title: 'Endereço de Entrega' }
},
// Step 2: Método de pagamento
{
path: 'payment',
canActivate: [stepGuard(1)], // Só acessa se step anterior completo
loadComponent: () => import('./steps/payment/payment.component'),
data: { step: 2, title: 'Pagamento' }
},
// Step 3: Confirmação
{
path: 'review',
canActivate: [stepGuard(2)],
loadComponent: () => import('./steps/review/review.component'),
resolve: {
orderSummary: () => inject(CheckoutService).getOrderSummary()
},
data: { step: 3, title: 'Revisar Pedido' }
},
// Confirmação final
{
path: 'success/:orderId',
loadComponent: () => import('./success/success.component'),
resolve: {
order: (route: ActivatedRouteSnapshot) =>
inject(OrderService).getById(route.params['orderId'])
}
}
]
}
];Por que essa arquitetura é poderosa:
Flow controlado: Guards garantem progressão correta
Estado preservado: Layout compartilhado mantém contexto
UX otimizada: Resolvers evitam loading states durante navegação
Padrão Avançado: Micro-frontends com Module Federation
Agora vamos explorar um padrão avançado que demonstra uso master-level.
O Problema com Monólitos de Frontend
// ❌ Limitações da abordagem simples
const routes: Routes = [
{ path: 'products', loadChildren: () => import('./products/products.module') },
{ path: 'orders', loadChildren: () => import('./orders/orders.module') },
{ path: 'analytics', loadChildren: () => import('./analytics/analytics.module') }
// Todas as features no mesmo repositório = deploy coupling
];Por que isso se torna problemático:
Equipes diferentes não podem deployar independentemente
Builds longos mesmo para mudanças pequenas
Shared dependencies causam conflitos de versão
Construindo a Solução com Module Federation
Estágio 1: Configuração do Shell Application
// shell-app/webpack.config.js
const ModuleFederationPlugin = require('@module-federation/webpack');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
'products-mf': 'products@http://localhost:4201/remoteEntry.js',
'orders-mf': 'orders@http://localhost:4202/remoteEntry.js',
'analytics-mf': 'analytics@http://localhost:4203/remoteEntry.js'
},
shared: {
'@angular/core': { singleton: true, strictVersion: true },
'@angular/common': { singleton: true, strictVersion: true },
'@angular/router': { singleton: true, strictVersion: true }
}
})
]
};
// shell-app/src/app/app.routes.ts
export const routes: Routes = [
{
path: 'products',
loadChildren: () => import('products-mf/Routes').then(m => m.routes),
data: {
microfrontend: 'products',
fallback: () => import('./fallbacks/products-fallback.component')
}
},
{
path: 'orders',
loadChildren: () => import('orders-mf/Routes').then(m => m.routes)
.catch(() => import('./fallbacks/orders-fallback.component')),
data: { microfrontend: 'orders' }
},
{
path: 'analytics',
loadChildren: () => import('analytics-mf/Routes').then(m => m.routes),
canLoad: [() => inject(FeatureToggleService).isEnabled('analytics')],
data: { microfrontend: 'analytics' }
}
];Module Federation deep dive:
O que faz: Permite carregar código de aplicações separadas em runtime
Por que é poderoso: Deploy independente + shared dependencies otimizadas
Quando usar: Teams grandes, features independentes, releases frequentes
Estágio 2: Micro-frontend Products
// products-mf/webpack.config.js
const ModuleFederationPlugin = require('@module-federation/webpack');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'products',
filename: 'remoteEntry.js',
exposes: {
'./Routes': './src/app/app.routes.ts'
},
shared: {
'@angular/core': { singleton: true },
'@angular/common': { singleton: true },
'@angular/router': { singleton: true }
}
})
]
};
// products-mf/src/app/app.routes.ts
export const routes: Routes = [
{
path: '',
loadComponent: () => import('./product-shell.component'),
children: [
{
path: '',
loadComponent: () => import('./views/catalog/catalog.component')
},
{
path: 'details/:id',
loadComponent: () => import('./views/details/details.component'),
resolve: {
product: (route: ActivatedRouteSnapshot) =>
inject(ProductService).getProduct(route.params['id'])
}
}
]
}
];Padrões de integração:
Comunicação via Router: Estado compartilhado através de query params
Event Bus: Custom events para comunicação cross-microfrontend
Shared Services: Injetados via dependency injection
Estágio 3: Error Handling e Fallbacks
// shell-app/src/app/services/microfrontend-loader.service.ts
@Injectable({
providedIn: 'root'
})
export class MicrofrontendLoaderService {
private failedLoads = new Set<string>();
async loadMicrofrontend(name: string): Promise<any> {
if (this.failedLoads.has(name)) {
return this.loadFallback(name);
}
try {
const module = await import(`${name}-mf/Routes`);
return module.routes;
} catch (error) {
console.warn(`Failed to load ${name} microfrontend:`, error);
this.failedLoads.add(name);
return this.loadFallback(name);
}
}
private async loadFallback(name: string) {
switch (name) {
case 'products':
return import('./fallbacks/products-fallback.routes');
case 'orders':
return import('./fallbacks/orders-fallback.routes');
default:
return import('./fallbacks/generic-fallback.routes');
}
}
}
// Rota com fallback automático
{
path: 'products',
loadChildren: () => inject(MicrofrontendLoaderService).loadMicrofrontend('products'),
data: { microfrontend: 'products' }
}Por que essa arquitetura é robusta:
Resiliente a falhas: Fallbacks automáticos quando micro-frontends não estão disponíveis
Deploy independente: Cada time pode deployar sem afetar outros
Escalabilidade: Novos micro-frontends podem ser adicionados sem rebuild
Roteamento com TypeScript Avançado
Para usuários TypeScript, aqui está como deixar tudo type-safe:
Configurando Types Robustos
// types/routing.ts
import { Data, Route } from '@angular/router';
export interface RouteData extends Data {
title?: string;
description?: string;
preload?: boolean;
priority?: 'low' | 'normal' | 'high';
requiresAuth?: boolean;
roles?: string[];
microfrontend?: string;
cache?: boolean;
fallback?: () => Promise<any>;
}
export interface AppRoute extends Omit<Route, 'data'> {
data?: RouteData;
children?: AppRoute[];
}
// Utility types para resolvers
export interface ProductResolverData {
product: Product;
recommendations: Product[];
reviews: Review[];
}
export type RouteResolvers<T = any> = {
[K in keyof T]: (route: ActivatedRouteSnapshot) => Observable<T[K]> | Promise<T[K]> | T[K];
};Benefícios de type safety:
Autocomplete: IDE sugere propriedades disponíveis
Compile-time checks: Erros detectados antes do runtime
Implementação com Tipagem Adequada
// routes/typed-routes.ts
import { inject } from '@angular/core';
import { AppRoute, ProductResolverData, RouteResolvers } from '../types/routing';
import { ProductService } from '../services/product.service';
const productResolvers: RouteResolvers<ProductResolverData> = {
product: (route) => inject(ProductService).getById(route.params['id']),
recommendations: (route) => inject(ProductService).getRecommendations(route.params['id']),
reviews: (route) => inject(ProductService).getReviews(route.params['id'])
};
export const productRoutes: AppRoute[] = [
{
path: 'details/:id',
loadComponent: () => import('./product-details.component'),
resolve: productResolvers,
data: {
title: 'Detalhes do Produto',
preload: true,
priority: 'high',
cache: true
}
}
];Patterns TypeScript Avançados
// guards/typed-guards.ts
import { CanActivateFn, CanMatchFn } from '@angular/router';
// Guard factory com types
export function createRoleGuard(roles: string[]): CanActivateFn {
return () => {
const authService = inject(AuthService);
return authService.hasAnyRole(roles);
};
}
// Guard composto
export function createCompositeGuard(
guards: CanActivateFn[]
): CanActivateFn {
return (route, state) => {
return guards.every(guard => guard(route, state));
};
}
// Uso tipado
const adminGuard = createRoleGuard(['admin', 'super-admin']);
const secureGuard = createCompositeGuard([authGuard, adminGuard]);
export const adminRoutes: AppRoute[] = [
{
path: 'admin',
canActivate: [secureGuard],
loadChildren: () => import('./admin/admin.routes'),
data: {
requiresAuth: true,
roles: ['admin'],
title: 'Painel Administrativo'
}
}
];Padrões Avançados e Melhores Práticas
1. Strategy Pattern para Preloading
O que resolve: Diferentes comportamentos de preload baseados em contexto
Como funciona: Interface comum com implementações específicas
// strategies/preload-strategies.ts
interface PreloadStrategy {
shouldPreload(route: Route): boolean;
getDelay(route: Route): number;
}
class NetworkAwareStrategy implements PreloadStrategy {
shouldPreload(route: Route): boolean {
// @ts-ignore - navigator.connection é experimental
const connection = navigator.connection;
if (connection?.effectiveType === '4g' && route.data?.['priority'] === 'high') {
return true;
}
return route.data?.['preload'] === true && connection?.effectiveType !== 'slow-2g';
}
getDelay(route: Route): number {
// @ts-ignore
const connection = navigator.connection;
return connection?.effectiveType === '4g' ? 500 : 2000;
}
}
class UserBehaviorStrategy implements PreloadStrategy {
private readonly analytics = inject(AnalyticsService);
shouldPreload(route: Route): boolean {
const path = route.path;
const userProbability = this.analytics.getNavigationProbability(path);
return userProbability > 0.7; // 70% de chance de navegação
}
getDelay(): number {
return 1000;
}
}
// Implementação da estratégia composta
@Injectable()
export class SmartPreloadStrategy implements PreloadingStrategy {
private strategies: PreloadStrategy[] = [
new NetworkAwareStrategy(),
new UserBehaviorStrategy()
];
preload(route: Route, load: () => Observable<any>): Observable<any> {
const shouldPreload = this.strategies.some(strategy =>
strategy.shouldPreload(route)
);
if (!shouldPreload) {
return of(null);
}
const delay = Math.min(
...this.strategies.map(strategy => strategy.getDelay(route))
);
return timer(delay).pipe(mergeMap(() => load()));
}
}Quando usar: Aplicações com requisitos complexos de preload baseados em contexto
2. Router State Management
O problema: Estado perdido durante navegação
A solução: Store centralizado para estado de navegação
// services/router-state.service.ts
@Injectable({
providedIn: 'root'
})
export class RouterStateService {
private state = new BehaviorSubject<any>({});
private history: string[] = [];
constructor(private router: Router) {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
map(event => (event as NavigationEnd).url)
).subscribe(url => {
this.history.push(url);
// Manter apenas os últimos 10
if (this.history.length > 10) {
this.history.shift();
}
});
}
setState(key: string, value: any): void {
const currentState = this.state.value;
this.state.next({ ...currentState, [key]: value });
}
getState(key: string): any {
return this.state.value[key];
}
canGoBack(): boolean {
return this.history.length > 1;
}
goBack(): void {
if (this.canGoBack()) {
const previousUrl = this.history[this.history.length - 2];
this.router.navigateByUrl(previousUrl);
}
}
}
// Uso em componentes
@Component({
template: `
<button (click)="goBack()" [disabled]="!canGoBack()">
Voltar
</button>
`
})
export class NavigationComponent {
private routerState = inject(RouterStateService);
get canGoBack() {
return this.routerState.canGoBack();
}
goBack() {
this.routerState.goBack();
}
}Benefícios: Estado preservado, navegação inteligente, melhor UX
3. Route Data Caching
Caso de uso: Evitar re-requests desnecessários em dados que não mudam frequentemente
// services/route-cache.service.ts
@Injectable({
providedIn: 'root'
})
export class RouteCacheService {
private cache = new Map<string, { data: any; timestamp: number; ttl: number }>();
set<T>(key: string, data: T, ttlMinutes: number = 5): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
ttl: ttlMinutes * 60 * 1000
});
}
get<T>(key: string): T | null {
const cached = this.cache.get(key);
if (!cached) {
return null;
}
if (Date.now() - cached.timestamp > cached.ttl) {
this.cache.delete(key);
return null;
}
return cached.data;
}
invalidate(pattern?: string): void {
if (!pattern) {
this.cache.clear();
return;
}
for (const key of this.cache.keys()) {
if (key.includes(pattern)) {
this.cache.delete(key);
}
}
}
}
// Resolver com cache
export const cachedProductResolver = (route: ActivatedRouteSnapshot) => {
const cache = inject(RouteCacheService);
const productService = inject(ProductService);
const productId = route.params['id'];
const cacheKey = `product-${productId}`;
// Verifica cache primeiro
const cached = cache.get(cacheKey);
if (cached) {
return of(cached);
}
// Se não tem cache, busca e armazena
return productService.getById(productId).pipe(
tap(product => cache.set(cacheKey, product, 10)) // Cache por 10 minutos
);
};Red flags: Cache muito agressivo pode mostrar dados desatualizados
4. Progressive Enhancement
O conceito: Aplicação funciona mesmo com JavaScript desabilitado
// services/progressive-enhancement.service.ts
@Injectable({
providedIn: 'root'
})
export class ProgressiveEnhancementService {
private supportsHistory = typeof window !== 'undefined' &&
window.history && window.history.pushState;
enhanceNavigation(): void {
if (!this.supportsHistory) {
return; // Fallback para navegação tradicional
}
// Intercepta clicks em links e converte para navegação SPA
document.addEventListener('click', (event) => {
const target = event.target as HTMLElement;
const link = target.closest('a[href]') as HTMLAnchorElement;
if (link && this.shouldEnhance(link)) {
event.preventDefault();
inject(Router).navigateByUrl(link.href);
}
});
}
private shouldEnhance(link: HTMLAnchorElement): boolean {
// Não enhance links externos
if (link.hostname !== window.location.hostname) {
return false;
}
// Não enhance downloads
if (link.download) {
return false;
}
return true;
}
}Trade-offs: Complexidade adicional vs melhor acessibilidade e SEO
Armadilhas Comuns a Evitar
1. Over-Engineering do Lazy Loading
O problema: Lazy loading em excesso para aplicações pequenas
// ❌ Não faça isso - lazy loading desnecessário para apps pequenos
const routes: Routes = [
{
path: 'simple-page',
loadComponent: () => import('./simple-page.component') // Componente de 2KB
}
];
// ✅ Faça isso - direto para componentes simples
const routes: Routes = [
{
path: 'simple-page',
component: SimplePageComponent // Componente já carregado
}
];Por que isso importa: Overhead de lazy loading pode ser maior que o benefício para componentes pequenos
2. Resolvers Bloqueantes
Erro comum: Resolvers que demoram demais para resolver
Por que acontece: Requests lentos ou sequenciais desnecessários
// ❌ Evite isso - resolvers que bloqueiam navegação
export const slowResolver = (route: ActivatedRouteSnapshot) => {
const service = inject(SlowService);
// Request lento que bloqueia navegação
return service.getSlowData(route.params['id']).pipe(
delay(5000) // 5 segundos de bloqueio!
);
};
// ✅ Solução - loading assíncrono no componente
@Component({
template: `
<div *ngIf="loading">Carregando...</div>
<div *ngIf="data">{{ data | json }}</div>
`
})
export class FastComponent implements OnInit {
loading = true;
data: any;
ngOnInit() {
const id = inject(ActivatedRoute).snapshot.params['id'];
inject(SlowService).getSlowData(id).subscribe(data => {
this.data = data;
this.loading = false;
});
}
}Prevenção: Use resolvers apenas para dados críticos, loading states para o resto
3. Memory Leaks em Lazy Modules
A armadilha: Subscriptions não canceladas em módulos lazy
// ❌ Evite isso - subscription que vaza memória
@Component({
template: `<div>{{ data$ | async }}</div>`
})
export class LeakyComponent implements OnInit {
data$!: Observable<any>;
ngOnInit() {
// Subscription que nunca é cancelada
this.service.getData().subscribe(data => {
// Processamento que pode vazar
});
}
}
// ✅ Solução - cleanup automático
@Component({
template: `<div>{{ data$ | async }}</div>`
})
export class CleanComponent implements OnDestroy {
private destroy$ = new Subject<void>();
data$!: Observable<any>;
ngOnInit() {
this.data$ = this.service.getData().pipe(
takeUntil(this.destroy$)
);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}Sinais de alerta: Uso de memória crescendo em navegação repetida
Quando NÃO Usar Lazy Loading
Não use lazy loading quando:
Aplicações muito pequenas: Menos de 5 rotas principais
Features sempre usadas juntas: Dashboard com widgets interdependentes
Latência crítica: Aplicações onde cada milissegundo importa
// ❌ Overkill para cenários simples
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.routes') // Overhead desnecessário
}
];
// ✅ Solução simples é melhor
const routes: Routes = [
{
path: 'dashboard',
component: DashboardComponent, // Carregamento direto
children: [
{ path: 'overview', component: OverviewComponent },
{ path: 'stats', component: StatsComponent }
]
}
];Framework de decisão: Use lazy loading quando bundle > 1MB ou features independentes
Roteamento vs Redux/NgRx
Quando Roteamento Brilha
Roteamento é ótimo para:
State baseado em URL: Filtros, paginação, navegação
Navegação simples: Entre páginas e features
SEO e deep linking: URLs amigáveis e compartilháveis
Quando Considerar Alternativas
Considere state management quando você precisa:
Estado complexo cross-component → NgRx: Para state complexo que sobrevive à navegação
Real-time updates → WebSockets + RxJS: Para dados que atualizam em tempo real
Optimistic updates → Apollo Client: Para aplicações GraphQL com cache inteligente
Matriz de Comparação
Característica | Router Angular | NgRx | Context API |
|---|---|---|---|
State em URL | ✅ Excelente | ❌ Não suporta | ❌ Não suporta |
Performance | ✅ Lazy loading | ⚠️ Overhead inicial | ✅ Leve |
Complexidade | ✅ Simples | ❌ Curva de aprendizado | ✅ Simples |
Debug | ✅ URL debugging | ✅ DevTools | ⚠️ Limitado |
Time travel | ❌ Não suporta | ✅ Excelente | ❌ Não suporta |
Conclusão
Roteamento no Angular 17+ é uma ferramenta poderosa que pode transformar aplicações lentas em experiências ultra-rápidas. Ele traz lazy loading inteligente, preload estratégico e arquiteturas escaláveis para suas SPAs.
Principais takeaways:
Lazy loading é essencial: Mas apenas para features independentes e aplicações grandes
Preload estratégico: Use dados de comportamento do usuário para otimizar carregamento
Guards funcionais: Mais limpos e testáveis que classes tradicionais
TypeScript all the way: Type safety evita bugs em runtime e melhora DX
Na próxima vez que você construir uma aplicação Angular, lembre-se dessas estratégias. Seus usuários vão notar a diferença na velocidade, e sua equipe vai agradecer pela arquitetura limpa.
Próximos passos:
Analise sua aplicação atual e identifique oportunidades de lazy loading
Implemente uma estratégia de preload baseada em analytics
Configure type safety completa em suas rotas
Você já implementou lazy loading em seus projetos? Que padrões têm funcionado melhor para você? Compartilhe sua experiência nos comentários!
Se este guia ajudou você a dominar roteamento no Angular, siga para mais patterns e best practices avançadas! 🚀
