Estratégias avançadas de cache em aplicações Angular para melhorar performance
Estratégias avançadas de cache em Angular podem elevar significativamente a performance e a experiência do usuário. Neste artigo, mostro como implementar caching inteligente usando RxJS e HTTP interceptors para otimizar requisições e reduzir latência.

Imagine uma aplicação Angular que consome múltiplos serviços REST para carregar dados essenciais. Um problema frequente é o excesso de requisições idênticas ao backend, causando lentidão perceptível e sobrecarga desnecessária no servidor. Na minha experiência, adotar estratégias avançadas de cache no frontend não só melhora a performance, como reduz custos e torna a aplicação mais responsiva.
Antes de entrar na teoria, vamos ver um exemplo simples que mostra como um cache básico pode ser implementado usando RxJS para armazenar o resultado de uma requisição HTTP.
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, shareReplay } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class DataService {
private cache$: Observable<MyData> | null = null;
constructor(private http: HttpClient) {}
getData(): Observable<MyData> {
if (!this.cache$) {
this.cache$ = this.http.get<MyData>('/api/data').pipe(
shareReplay(1) // Compartilha a mesma resposta entre múltiplos assinantes
);
}
return this.cache$;
}
}
Neste exemplo, shareReplay(1) garante que a resposta seja compartilhada e armazenada. Assim, várias chamadas a getData() usarão o resultado cacheado enquanto a aplicação estiver ativa. Esse padrão simples resolve o problema de requisições duplicadas em uma sessão.
Agora, vamos complementar com algumas ideias que expandem o conceito e tornam o cache mais flexível e robusto:
1. Cache com TTL (Time to live)
Cachear indefinidamente pode causar problemas quando os dados no backend mudam. Para isso, prefiro incluir TTL, que invalida o cache após um período.
interface CacheEntry<T> {
expiration: number;
observable: Observable<T>;
}
@Injectable({ providedIn: 'root' })
export class DataServiceWithTTL {
private cacheEntry: CacheEntry<MyData> | null = null;
private ttl = 5 * 60 * 1000; // 5 minutos
constructor(private http: HttpClient) {}
getData(): Observable<MyData> {
const now = Date.now();
if (!this.cacheEntry || this.cacheEntry.expiration < now) {
const observable = this.http.get<MyData>('/api/data').pipe(shareReplay(1));
this.cacheEntry = { observable, expiration: now + this.ttl };
}
return this.cacheEntry.observable;
}
}
Com isso, o cache serve para otimizar chamadas recentes, mas garante que a aplicação eventualmente faça uma nova consulta para manter os dados atualizados.
2. Cache seletivo com HTTP interceptor
Em sistemas mais complexos, reorganizo o cache em um componente global, como um interceptor HTTP. Ele permite interceptar requisições, validar o cache e até responder diretamente com dados armazenados.
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { tap, shareReplay } from 'rxjs/operators';
@Injectable()
export class CacheInterceptor implements HttpInterceptor {
private cache = new Map<string, { expiration: number; response$: Observable<HttpEvent<any>> }>();
private ttl = 300000; // 5 minutos
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.method !== 'GET') {
return next.handle(req);
}
const cached = this.cache.get(req.urlWithParams);
const now = Date.now();
if (cached && cached.expiration > now) {
return cached.response$;
}
const response$ = next.handle(req).pipe(
shareReplay(1),
tap(() => {
this.cache.set(req.urlWithParams, { expiration: now + this.ttl, response$ });
})
);
return response$;
}
}
O interceptor faz cache automático para todas requisições GET, reduzindo preocupações no serviço. Também ajuda na manutenção centralizada do comportamento de cache.
3. Cache com invalidação baseada em eventos
Em aplicações interativas, dados podem mudar devido a operações do usuário, criando a necessidade de invalidar o cache quando certas ações ocorrem.
Eu sugiro combinar o interceptor com um serviço de eventos, por exemplo usando Subject ou BehaviorSubject, para emitir notificações de invalidação que limpam o cache:
import { Subject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class CacheInvalidationService {
private invalidate$ = new Subject<void>();
get invalidation$() {
return this.invalidate$.asObservable();
}
invalidateCache() {
this.invalidate$.next();
}
}
E no interceptor:
constructor(private invalidationService: CacheInvalidationService) {
this.invalidationService.invalidation$.subscribe(() => this.cache.clear());
}
Assim, após operações como atualização de dados, o cache pode ser automaticamente limpo para garantir consistência.
Quando usar essas estratégias
- Cache básico e TTL são recomendados para quase todo frontend que consome APIs REST, reduzindo chamadas desnecessárias.
- Interceptor global funciona muito bem em sistemas que acessam muitas APIs e querem gerenciar cache de forma transversal.
- Cache com invalidação por evento faz sentido em aplicativos com operações mutativas frequentes e que exigem alta consistência.
No entanto, evite cachear dados críticos que mudam continuamente ou que podem afetar a segurança, como dados financeiros sensíveis, sem estratégia clara de atualização.
Próximos passos
Explorar a integração dessas estratégias com armazenamento local (localStorage, IndexedDB) pode permitir cache persistente entre sessões. Também recomendo estudar o padrão Stale-While-Revalidate (SWR) aplicado a Angular para respostas imediatas e atualização em segundo plano.
Documentação oficial para aprofundar:
- Angular HTTP: https://angular.io/guide/http
- RxJS shareReplay: https://rxjs.dev/api/operators/shareReplay
- Angular Interceptors: https://angular.io/guide/http#intercepting-requests
Implementar cache avançado não é trivial, mas com essas práticas elevamos substancialmente a responsividade e escalabilidade de aplicações Angular.