Skip to main content

Angular Caching

Introduction

Caching is a technique used to store copies of data temporarily in a location that allows faster retrieval. In web applications, caching can significantly improve performance by reducing the need to make repeated HTTP requests to the server for the same data.

Angular provides built-in mechanisms to implement caching for HTTP requests. By implementing an effective caching strategy, your Angular applications can:

  • Load faster
  • Reduce network traffic
  • Decrease server load
  • Improve user experience when network connectivity is poor

In this tutorial, we'll explore how to implement different caching strategies in Angular applications using the HttpClient service.

Prerequisites

Before diving into Angular caching, make sure you are familiar with:

  • Basic Angular concepts
  • Angular's HttpClient module
  • Observables and RxJS operators

Understanding HTTP Caching in Angular

Angular's HttpClient doesn't provide automatic caching out of the box. However, it gives us the tools to implement custom caching strategies using RxJS operators and services.

Why Use Caching?

Consider a scenario where your application needs to display a list of products fetched from an API. Without caching, every time a user navigates to the products page, a new HTTP request is made, even if the data hasn't changed.

With caching:

  1. First visit: Data is fetched from the server and stored in memory
  2. Subsequent visits: Data is retrieved from the cache instead of making a new HTTP request

Basic In-Memory Caching

Let's start with a simple in-memory caching implementation using a service:

typescript
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap, shareReplay } from 'rxjs/operators';

@Injectable({
providedIn: 'root'
})
export class CacheService {
private cache: { [key: string]: any } = {};

constructor(private http: HttpClient) { }

get(url: string, options?: any): Observable<any> {
// If the URL exists in cache and is not expired, return the cached data
if (this.cache[url]) {
return of(this.cache[url]);
}

// Otherwise, make the HTTP request and store the result in cache
return this.http.get(url, options).pipe(
tap(response => {
this.cache[url] = response;
})
);
}

// Method to clear the cache
clearCache(url?: string): void {
if (url) {
delete this.cache[url];
} else {
this.cache = {};
}
}
}

Using the Cache Service

Here's how you can use this cache service in a component:

typescript
import { Component, OnInit } from '@angular/core';
import { CacheService } from './cache.service';

@Component({
selector: 'app-products',
template: `
<div *ngIf="products">
<div *ngFor="let product of products">
<h3>{{ product.name }}</h3>
<p>{{ product.description }}</p>
<p>Price: ${{ product.price }}</p>
</div>
</div>
<button (click)="refreshProducts()">Refresh Products</button>
`
})
export class ProductsComponent implements OnInit {
products: any[] = [];

constructor(private cacheService: CacheService) { }

ngOnInit(): void {
this.loadProducts();
}

loadProducts(): void {
this.cacheService.get('https://api.example.com/products').subscribe(
data => {
console.log('Products loaded!');
this.products = data;
},
error => console.error('Error loading products', error)
);
}

refreshProducts(): void {
// Clear the cache before reloading to force a fresh request
this.cacheService.clearCache('https://api.example.com/products');
this.loadProducts();
}
}

Advanced Caching with RxJS Operators

The previous example works, but we can improve it using RxJS operators like shareReplay() to handle multiple subscriptions more efficiently.

typescript
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { shareReplay } from 'rxjs/operators';

@Injectable({
providedIn: 'root'
})
export class AdvancedCacheService {
private cache: { [key: string]: Observable<any> } = {};

constructor(private http: HttpClient) { }

get(url: string, options?: any): Observable<any> {
if (!this.cache[url]) {
// Store the Observable, not the data itself
this.cache[url] = this.http.get(url, options).pipe(
// Cache the most recent emitted value
// Buffer size 1 means we keep the last value
// refCount: false means the cached observable won't be unsubscribed
// when there are no more subscribers
shareReplay(1, 30 * 60 * 1000) // Cache for 30 minutes
);
}

return this.cache[url];
}

clearCache(url?: string): void {
if (url) {
delete this.cache[url];
} else {
this.cache = {};
}
}
}

Benefits of Using shareReplay()

  1. Multiple subscribers share one request: If multiple components request the same data simultaneously, only one HTTP request is made
  2. Automatic response caching: The most recent emission from the observable is cached
  3. Time-based expiration: We can set a window time for the cache to be valid

Implementing Cache Expiration

For many real-world applications, you'll want caches to expire after some time to ensure data freshness. Let's enhance our service with expiration functionality:

typescript
interface CacheEntry {
expiresAt: number;
value: any;
}

@Injectable({
providedIn: 'root'
})
export class TimedCacheService {
private cache: { [url: string]: CacheEntry } = {};

constructor(private http: HttpClient) { }

get(url: string, ttl: number = 60000): Observable<any> {
const now = Date.now();

// Check if cache exists and is still valid
if (this.cache[url] && this.cache[url].expiresAt > now) {
return of(this.cache[url].value);
}

// If not in cache or expired, make the request and store it
return this.http.get(url).pipe(
tap(response => {
this.cache[url] = {
expiresAt: now + ttl,
value: response
};
})
);
}

clearCache(url?: string): void {
if (url) {
delete this.cache[url];
} else {
this.cache = {};
}
}
}

HTTP Cache Headers with Angular Interceptors

Angular's HttpClient respects HTTP caching headers from the server. We can enhance our caching strategy by using HTTP interceptors to work with these headers:

typescript
import { Injectable } from '@angular/core';
import {
HttpEvent,
HttpInterceptor,
HttpHandler,
HttpRequest,
HttpResponse
} from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap, share } from 'rxjs/operators';

@Injectable()
export class CacheInterceptor implements HttpInterceptor {
private cache = new Map<string, HttpResponse<any>>();

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Only cache GET requests
if (request.method !== 'GET') {
return next.handle(request);
}

// Check if we have a cached response
const cachedResponse = this.cache.get(request.url);
if (cachedResponse) {
return of(cachedResponse.clone());
}

// If not cached, make the request and cache the response
return next.handle(request).pipe(
tap(event => {
if (event instanceof HttpResponse) {
this.cache.set(request.url, event.clone());
}
}),
share()
);
}

clearCache(url?: string): void {
if (url) {
this.cache.delete(url);
} else {
this.cache.clear();
}
}
}

Add this interceptor to your providers in app.module.ts:

typescript
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { CacheInterceptor } from './cache.interceptor';

@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: CacheInterceptor,
multi: true
}
]
})
export class AppModule { }

Real-World Example: User Profile Caching

Let's implement a complete real-world example for caching user profile data:

typescript
// user.model.ts
export interface User {
id: number;
name: string;
email: string;
role: string;
lastLoginDate: Date;
}

// user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { tap, catchError, shareReplay } from 'rxjs/operators';
import { User } from './user.model';

@Injectable({
providedIn: 'root'
})
export class UserService {
private apiUrl = 'https://api.example.com/users';
private userCache: { [id: number]: Observable<User> } = {};

constructor(private http: HttpClient) { }

// Get a user with caching
getUser(id: number): Observable<User> {
if (!this.userCache[id]) {
// Set up the cache for this specific user ID
this.userCache[id] = this.http.get<User>(`${this.apiUrl}/${id}`).pipe(
catchError(error => {
delete this.userCache[id]; // Remove failed requests from cache
return throwError(error);
}),
// Cache the last result for 10 minutes (600,000 ms)
shareReplay(1, 600000)
);
}

return this.userCache[id];
}

// Force refresh a user's data
refreshUser(id: number): Observable<User> {
// Delete from cache first
delete this.userCache[id];
// Then fetch fresh data
return this.getUser(id);
}

// Update a user and update the cache
updateUser(user: User): Observable<User> {
return this.http.put<User>(`${this.apiUrl}/${user.id}`, user).pipe(
tap(updatedUser => {
// Update the cache with the new user data
delete this.userCache[user.id]; // Remove old cached data
this.userCache[user.id] = of(updatedUser); // Add updated data to cache
})
);
}
}

Now let's use this service in a component:

typescript
// user-profile.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { UserService } from './user.service';
import { User } from './user.model';
import { finalize } from 'rxjs/operators';

@Component({
selector: 'app-user-profile',
template: `
<div *ngIf="loading">Loading user data...</div>

<div *ngIf="user && !loading" class="user-profile">
<h2>{{ user.name }}</h2>
<p>Email: {{ user.email }}</p>
<p>Role: {{ user.role }}</p>
<p>Last Login: {{ user.lastLoginDate | date }}</p>

<button (click)="refreshUserData()">Refresh</button>
</div>

<div *ngIf="error" class="error">
{{ error }}
</div>
`
})
export class UserProfileComponent implements OnInit {
user: User | null = null;
loading = false;
error: string | null = null;

constructor(
private route: ActivatedRoute,
private userService: UserService
) { }

ngOnInit(): void {
const userId = +this.route.snapshot.paramMap.get('id')!;
this.loadUser(userId);
}

loadUser(id: number): void {
this.loading = true;
this.error = null;

this.userService.getUser(id).pipe(
finalize(() => this.loading = false)
).subscribe(
user => this.user = user,
error => this.error = 'Failed to load user data: ' + error.message
);
}

refreshUserData(): void {
if (!this.user) return;

this.loading = true;
this.error = null;

this.userService.refreshUser(this.user.id).pipe(
finalize(() => this.loading = false)
).subscribe(
user => this.user = user,
error => this.error = 'Failed to refresh user data: ' + error.message
);
}
}

Optimizing Caching Strategies

Depending on your application's needs, you might want to implement different caching strategies:

1. Cache-First Strategy

Always check the cache first. Only fetch from the server if the cache is empty or expired.

typescript
getCacheFirst(url: string, ttl: number = 60000): Observable<any> {
// Check cache first
if (this.hasValidCachedValue(url, ttl)) {
return of(this.cache[url].value);
}

// Then go to network
return this.getFromNetwork(url);
}

2. Network-First Strategy

Always try to fetch fresh data from the server first. Fall back to cache if the network request fails.

typescript
getNetworkFirst(url: string): Observable<any> {
return this.http.get(url).pipe(
tap(response => {
// Update the cache with fresh data
this.setCache(url, response);
}),
catchError(error => {
// On error, try to get from cache
if (this.cache[url]) {
console.log('Network request failed, returning cached data');
return of(this.cache[url].value);
}
// If no cache, propagate the error
return throwError(error);
})
);
}

3. Stale-While-Revalidate Strategy

Immediately return cached data (even if stale), but fetch fresh data in the background to update the cache for next time.

typescript
getStaleWhileRevalidate(url: string): Observable<any> {
// Start the network request
const networkResponse = this.http.get(url).pipe(
tap(response => {
// Update cache in background
this.setCache(url, response);
}),
catchError(error => {
console.error('Background refresh failed:', error);
return EMPTY;
})
);

// Start the network request to update cache, but don't wait for it
networkResponse.subscribe();

// Immediately return cached response if available (even if stale)
if (this.cache[url]) {
return of(this.cache[url].value);
}

// If no cache exists yet, wait for the network response
return networkResponse;
}

Performance Considerations

When implementing caching in Angular applications, consider these performance tips:

  1. Cache Size Management: Implement a mechanism to limit the size of your cache to prevent memory issues
  2. Cache Invalidation: Develop a strategy for invalidating caches when data changes on the server
  3. Selective Caching: Not all endpoints should be cached. Identify which requests benefit most from caching
  4. Caching Headers: Respect and leverage HTTP caching headers from your server
  5. Offline Support: Consider combining caching with IndexedDB for robust offline capabilities

Summary

In this tutorial, we've explored different methods to implement HTTP caching in Angular applications:

  1. Basic in-memory caching
  2. RxJS-powered caching with shareReplay()
  3. Cache invalidation and expiration
  4. HTTP Interceptor-based caching
  5. Real-world implementation of user profile caching
  6. Different caching strategies for various use cases

By implementing appropriate caching strategies, you can significantly improve your Angular application's performance and user experience, especially for data that doesn't change frequently.

Additional Resources

Exercises

  1. Implement a cache service that automatically clears entries older than 1 hour
  2. Create a mechanism to pre-populate the cache with initial data when your application starts
  3. Modify the user profile example to show visual indicators when data is loaded from cache versus from the network
  4. Implement a caching interceptor that respects ETag and If-None-Match headers
  5. Build a dashboard that shows cache statistics (hit ratio, size, etc.) for debugging purposes

Happy coding!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)