Prefetch inteligente de rotas com Signals e RxJS em Standalone Components
Por John Pires · 11 de março de 2026

Cenario pratico do dia a dia
Problema observado em apps reais
Em aplicações Angular com rotas lazy e componentes standalone é comum perceber latência ao navegar para uma rota pela primeira vez: o bundle da rota é baixado e avaliado, causando um atraso perceptível. Usuários esperam transições instantâneas e interfaces responsivas; o prefetch de rotas é uma técnica direta para reduzir essa latência. Porém não é suficiente disparar tudo ao carregar a página — isso desperdiça banda e aumenta TTI.
O objetivo aqui é construir um mecanismo de prefetch inteligente: pré-carregar apenas rotas com alta probabilidade de uso, cancelar prefetchs redundantes, expor estado reativo para a UI e operar bem com standalone components e DI moderno do Angular.
Vamos mostrar um pattern que combina Signals (para estado síncrono e autoatualizável na tree), RxJS (para gestão de intenções, debouncing e cancelamento), e diretivas/serviços injetáveis para integrar com links e menus.
Exemplos de cenário prático
Casos típicos onde o prefetch inteligente traz ganhos reais:
- Usuário passa o mouse sobre um link em um dashboard: alta probabilidade de navegação.
- Links próximos à viewport em uma lista longa: prefetch quando o link entra na viewport.
- Navegação por teclado (focus): prefetch imediato no foco do elemento.
Esses pontos de sinalização (hover, focus, intersection) serão a base para nossas fontes de intenção. Usaremos RxJS para combinar e regular essas fontes, e Signals para exposição simples do estado.
Arquitetura da solucao
Componentes e responsabilidades
Arquitetura mínima necessária:
- PrefetchService: núcleo. Recebe intents, gerencia cache, inicia import() das rotas, expõe sinais com status (idle/pending/success/error).
- PrefetchIntentDirective: aplicada em anchors ou botões. Escuta hover/focus/click e emite intents para o serviço.
- Route config: loadComponent com dynamic import; o prefetch service grava que a rota foi carregada para evitar double fetch.
- Componente consumidor: pode ler signals para mostrar indicador de prefetch (ex: shimmer, hint).
Fluxo: diretiva detecta intenção -> envia um evento Rx para o serviço -> serviço combina/intensifica com debounce -> inicia prefetch (import) -> atualiza signal de estado -> rota efetua lazy load instantaneamente quando o usuário navegar.
Decisões técnicas
Decidi usar import() direto para pré-carregar o bundle da rota porque é compatível com o padrão de lazy loading mantido pelo Angular e evita dependências externas. Import não é abortável nativamente; para cancelar work flows usaremos RxJS switchMap para ignorar resultados de import anteriores e manter um cache booleano para evitar re-import redundante. A signal expõe o estado para o template sem precisar de subscriptions manuais.
Vamos também usar inject() e providers no estilo standalone para manter a solução compatível com componentes standalone e possibilitar tree-shaking.
Codigo completo com explicacoes
PrefetchService (o motor)
Contexto: serviço injetável que recebe uma chave de rota (string) e tenta pré-carregar o bundle correspondente com lógica de debounce, timeout e cache.
import { Injectable, computed, signal } from '@angular/core';
import { Subject, from, of, timer } from 'rxjs';
import { switchMap, takeUntil, catchError, tap, filter } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class PrefetchService {
private intent$ = new Subject<{ key: string }>();
private abort$ = new Subject<string>();
// cache simples: key -> 'idle'|'pending'|'done'|'error'
private statuses = new Map<string, string>();
// signal exposta para templates
public statusSignal = signal(this.statuses);
constructor() {
this.intent$
.pipe(
// switchMap cancela fetches anteriores para a mesma chave
switchMap(intent => this.createPrefetchObservable(intent.key))
)
.subscribe();
}
trigger(key: string) {
// não reenfileira se já done
const s = this.statuses.get(key);
if (s === 'done' || s === 'pending') return;
this.intent$.next({ key });
}
private createPrefetchObservable(key: string) {
this.setStatus(key, 'pending');
// timeout de segurança
return from(this.dynamicImportByKey(key)).pipe(
tap(() => this.setStatus(key, 'done')),
catchError((err) => {
this.setStatus(key, 'error');
return of(null);
})
);
}
private async dynamicImportByKey(key: string) {
// mapear key -> import dependendo do seu app
// exemplo: '/profile' => import('../routes/profile/profile.component')
if (key === '/profile') {
return await import('../routes/profile/profile.component');
}
if (key === '/settings') {
return await import('../routes/settings/settings.component');
}
throw new Error('no-mapping');
}
private setStatus(key: string, status: string) {
this.statuses.set(key, status);
// atualizar signal com uma cópia para disparar reatividade
this.statusSignal.set(new Map(this.statuses));
}
}
O que acontece: o serviço mantém uma Subject de intents. Ao receber uma intent, marca como pending e chama a função de import dinâmica. Quando o bundle resolve, marca como done; em erro marca error. O signal é um Map que é substituído por cópia para notificar assinantes.
Diretiva de intenção (hover, focus, intersection)
Contexto: anexar a diretiva a elementos que representam navegação. Ela emite intent para o serviço quando o usuário demonstra intenção forte (hover + 150ms, focus imediato, ou quando o elemento entra na viewport).
import { Directive, ElementRef, HostListener, Input, inject, OnDestroy } from '@angular/core';
import { PrefetchService } from './prefetch.service';
import { fromEvent, Subscription, timer } from 'rxjs';
import { switchMap, takeUntil } from 'rxjs/operators';
@Directive({ selector: '[prefetchIntent]' })
export class PrefetchIntentDirective implements OnDestroy {
@Input('prefetchIntent') prefetchKey!: string;
private sub = new Subscription();
private svc = inject(PrefetchService);
constructor(private el: ElementRef<HTMLElement>) {
// intersection observer para prefetch quando entrou na viewport
const io = new IntersectionObserver(entries => {
for (const e of entries) {
if (e.isIntersecting) this.svc.trigger(this.prefetchKey);
}
}, { threshold: 0.5 });
io.observe(this.el.nativeElement);
// limpa observer no destroy
this.sub.add({ unsubscribe: () => io.disconnect() });
}
@HostListener('focus') onFocus() {
this.svc.trigger(this.prefetchKey);
}
@HostListener('mouseenter') onHover() {
// debounce simples: só dispare se estiver >=150ms sobre o item
const hover$ = timer(150).subscribe(() => this.svc.trigger(this.prefetchKey));
this.sub.add(hover$);
}
ngOnDestroy() {
this.sub.unsubscribe();
}
}
O que acontece: a diretiva combina triggers padrão (focus, hover com debounce) e IntersectionObserver. Sempre que a intenção é detectada, chama PrefetchService.trigger, delegando política e cache ao serviço.
Configuração de rota com loadComponent (standalone)
Contexto: exemplo de rota standalone que será pré-carregada. O key usado pelo serviço deve corresponder ao path/identificador que o diretiva/passará.
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'profile',
loadComponent: () => import('./routes/profile/profile.component').then(m => m.ProfileComponent)
},
{
path: 'settings',
loadComponent: () => import('./routes/settings/settings.component').then(m => m.SettingsComponent)
}
];
O que acontece: as rotas estão definidas como standalone components via loadComponent. O PrefetchService usa as mesmas import paths internamente (mapeamento) para pré-carregar os bundles correspondentes.
Tratamento de erros e edge cases
Erros comuns e armadilhas
Erros que aparecem frequentemente ao implementar prefetch:
- Import duplicado: chamar import() duas vezes para o mesmo bundle não quebra, mas desperdiça CPU e banda. Solução: cache no serviço (status 'done').
- Cancelamento inadequado: import() não é abortável. Usar switchMap apenas evita side-effects lógicos, mas não interrompe bytes sendo baixados. Se banda for crítica, prefetch via fetch do arquivo chunk + cache pode ser considerado (mais complexo).
- Mapeamento inconsistente: se o mapeamento key->import mudar, o prefetch pode carregar o bundle errado. Centralize mappings num único lugar.
Outra armadilha: confiar apenas em hover para dispositivos touch. Sempre combine com focus/touchstart e intersection para capturar a maioria dos casos.
Retries, timeouts e fallback
Implementar retry simples e timeout protege contra redes instáveis. Exemplo prático:
// dentro do PrefetchService.createPrefetchObservable
return from(this.dynamicImportByKey(key)).pipe(
// se falhar, tente 1 vez depois de 500ms
catchError(err => timer(500).pipe(switchMap(() => from(this.dynamicImportByKey(key))))),
timeout(5000),
catchError(err => { this.setStatus(key, 'error'); return of(null); })
);
O que acontece: uma tentativa extra é feita em erro; um timeout garante que não ficamos pendurados indefinidamente. Ajuste tempo conforme o ramo de deploy e tamanho dos chunks.
Dica: mantenha o mapeamento import em ambiente de build (por exemplo, gerar um arquivo map.json em build time) se você tem muitos pontos de rota. Isso evita divergências manuais entre rotas e prefetch.
Performance e otimizacao
Estratégias para reduzir custo de prefetch
Boas práticas para controlar o impacto do prefetch:
- Limitador global: nunca permita mais que N prefetches simultâneos. Use um semáforo no serviço.
- Heurísticas de rede: respeite
navigator.connection.effectiveTypee desative prefetch em conexões 2g ou quando save-data está ativo. - Cache persistente: use IndexedDB para armazenar assets críticos quando aplicável (avançado).
Implementação de checagem básica de rede:
private shouldPrefetch(): boolean {
const nav = (navigator as any);
if (nav.connection) {
const type = nav.connection.effectiveType;
if (type === '2g' || nav.connection.saveData) return false;
}
return true;
}
O que acontece: o serviço verifica a condição de rede antes de iniciar o import; é um gate simples que reduz tráfego em redes lentas.
Semaforo para limitar prefetches simultaneos
Exemplo: limitar a 3 prefetches concorrentes. Mantemos um contador e uma fila simples.
Implementação: crie uma fila FIFO, incremente contador em start, decremento em complete e processe fila.
Conclusao practica
Resumo direto: usar Signals para expor estado e RxJS para orquestração permite construir um prefetch inteligente e controlável que funciona bem com standalone components. A diretiva de intenção mantém a UX simples e desacoplada do serviço. O serviço centraliza cache, políticas (retries, timeout, heurísticas de rede) e evita duplicação de imports.
Checklist prático para implementação:
- Centralize mapeamento key -> import()
- Use signals para status e evitar subscriptions no template
- Combine hover/focus/intersection como fontes de intenção
- Adicione semáforo, heurísticas de rede e timeout para robustez
Com esses padrões você reduz latência de navegação sem sacrificar banda ou estabilidade. Comece pequeno: implemente prefetch apenas para 2-3 rotas de alta prioridade e observe métricas reais antes de ampliar.
Exemplos completos estão prontos para adaptar ao seu app: copie o PrefetchService, registre a diretiva em módulos/standalone components e mapeie rotas. Ajuste timers e políticas conforme telemetria.
Se quiser, posso gerar uma versão do PrefetchService com semáforo e heurística de rede já implementada, pronta para drop-in. Também posso mostrar como gerar um mapa de imports automaticamente no pipeline de build para evitar divergências manuais.
Assuntos: Angular, signals, rxjs, standalone-components, routing, dependency-injection, prefetch
Leia também
Providers com escopo de rota em Angular standalone: signals, ciclo de vida e limpeza
Guia prático para usar providers com escopo de rota em Angular standalone: exemplos com Signals, factories que leem params de rota, limpeza via onCleanup e…
Signals + RxJS em rotas lazy com Standalone Components e Route Providers: modo errado vs modo certo
Padrões práticos para combinar Signals e RxJS em rotas lazy com standalone components e route providers: comparativo, erros comuns e implementação correta…
Sincronizar filtros na URL com Angular: Signals + Router + RxJS (para iniciantes)
Aprenda a sincronizar filtros com a URL no Angular usando Signals, Router e RxJS: passo a passo para iniciantes, evitando loops de navegação, aplicando…
Migrando BehaviorSubject para Signal em formulários Angular — passo a passo
Guia prático para migrar BehaviorSubject para Signal em formulários Angular: exemplos errados e corretos, patterns com standalone components, DI e routing, e…
Wizard multi-step com rotas filhas, Signals e Standalone Components
Tutorial prático e direto para criar um wizard multi-step em Angular: rotas filhas para cada etapa, gerenciamento de estado com Signals e componentes…
Prefetch de dados por rota no Angular com Standalone Components e Signals
Aprenda a prefetch de dados por rota no Angular usando Standalone Components e Signals: combine ResolveFn com um PrefetchStore reativo para reduzir latência…