Optimizing Angular Performance with HttpInterceptor Caching
Do you ever wonder why web pages with gigabytes of data load in seconds? The secret behind this is caching. According to Google’s guidelines, a site should load within 3 seconds, which can be achieved through caching. This article will explain how to implement caching using HttpInterceptor.
Discover how at OpenReplay.com.
Caching creates temporary duplicate files or data that might be requested again, thus making subsequent requests for the information faster. Applications are accelerated by reducing load times and minimizing repeated data fetching from the main server as the system fetches data from the quick-access cache instead of the original slower source.
HttpInterceptor
is an Angular service that intercepts HTTP requests and responses, allowing for their modification. It performs tasks like logging, authenticating, and even caching for HTTP requests on a central point.
The intercept
method is the core of the HttpInterface
. This method takes two parameters: an HttpRequest
object (which represents the outgoing request) and a HttpHandler
(which processes the request and returns an observable of HTTP events).
Within the method, developers can change requests by adding headers, changing URLs, or modifying the body. After that, the request is forwarded to the next
handler in the chain.
RxJS operators like tap
are also used by interceptors to modify responses. This allows you to log your response, catch specific status codes, or even implement caching, thereby providing centralized management of common tasks across all HTTP requests and responses.
To activate an interceptor, it must be registered in the Angular module using the HTTP_INTERCEPTORS
token. This is done in the module’s providers
array, specifying the interceptor class and setting the multi
option to true, which allows multiple interceptors to be applied in sequence. The interceptor processes both requests and responses. Requests are processed serially, while the responses are processed in reverse order.
Benefits of Using HttpInterceptor for Caching
Using HttpInterceptor
for caching has several benefits, as it improves the user experience of your application. Some of these benefits include:
-
Centralized Caching Logic: Since the caching logic is implemented in one place, it helps reduce complexity in the components.
-
Improved Performance: Caching HTTP responses reduces server requests, resulting in faster load times and decreased server load.
-
Consistency: Ensures that the same caching strategy is applied uniformly across the entire application.
-
Enhanced User Experience: Speeds up access to previously fetched data, making the application more responsive.
-
Offline Support: When there is no network connection, it serves cached data offline.
Setting Up the Angular Project
Let’s scaffold a new Angular project, but before you follow along with this tutorial, ensure you have the following:
-
Node.js: Angular requires Node.js to be installed. Download it from the Node.js official website.
After properly setting up the prerequisites, create a new Angular project using the Angular Command Line Interface(CLI).
ng new caching-app
Navigate to the project directory.
cd caching-app
Run the application.
ng serve
Creating The Caching Logic
To begin, create a new folder named services
within the app
folder. This folder will hold all the service logic required for the interceptor.
Next, generate a new service named cache
using the following command:
ng generate service cache
Within the CacheService
class in the cache.service.ts
file, declare a private property cache
of type Map<string, any>
. This Map
will store cached data, with keys as strings and values of any type.
private cache: Map<string, any> = new Map<string, any>();
Next, define the put
, get
, and clear
methods.
- The
put
Method This method adds an item to the cache. It takes two parameters:key
(a string that serves as the identifier) andvalue
(any data). In the cache map, save the value under the key by usingthis.cache.set(key, value)
.
put(key: string, value: any): void {
this.cache.set(key, value);
}
- The
get
Method The get method returns the item from the cache. It usesthis.cache.get(key)
to retrieve the corresponding value from the cacheMap
, but if no item matches thekey
given, the method returnsundefined
.
get(key: string): any {
return this.cache.get(key);
}
- The
clear
Method The clear method removes all items from the cache by calling thethis.cache.clear()
function.
clear(): void {
this.cache.clear();
}
Creating a Basic HttpInterceptor
Implement the HttpInterceptor
interface to create a simple HttpInterceptor. Use this interface to intercept HTTP requests and responses between your application and the server.
First, create an http-interceptor.ts
file within the app
folder. In this file, import the necessary dependencies:
import { Injectable } from '@angular/core';
import { HttpEvent, HttpEventType, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { CacheService } from '../services/cache.service';
Next, define an Angular HttpInterceptor
named CachingInterceptor
. This interceptor will handle caching functionality. Use the @Injectable
decorator to make CachingInterceptor
an injectable service within Angular.
In the CachingInterceptor
class, inject an instance of CacheService
through the constructor
. This allows the CachingInterceptor
to interact with the cache service to store and retrieve cached responses.
@Injectable()
export class CachingInterceptor implements HttpInterceptor {
constructor(private cacheService: CacheService) {}
}
Within this component, create the intercept
method, which takes two parameters: The request
and the handler
.
export class CachingInterceptor implements HttpInterceptor {
constructor(private cacheService: CacheService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Implementation of caching logic will go here
}
}
We would add some checks in the intercept
method. First, we check if the request method is not GET
. If it is not, it forwards the request to the next
handler.
if (request.method !== 'GET') {
return next.handle(request);
}
Next, we attempt to retrieve a cached response for the request URL from CacheService
. If there is a cached response return it.
const cachedResponse = this.cacheService.get(request.url);
if (cachedResponse) {
return of(cachedResponse);
}
But when there is no cached response, the interceptor would forward the request to the next
handler. The response is then processed through an RxJS tap
operator.
Inside the tap
operator, check if the event type is HttpEventType.Response
, indicating that the response has been received from the server. If so, cache the response using this.cacheService.put(request.url, event)
.
return next.handle(request).pipe(
tap((event: HttpEvent<any>) => {
if (event.type === HttpEventType.Response) {
this.cacheService.put(request.url, event); // Cache the response
}
})
);
Integrating Caching into your app
Now that we’ve created the caching mechanism, we will create an application to see if it works. In this application, we would search for a particular item from the endpoint, and if it is found in the cache, it would print the information on the console.
We begin creating this application by defining the data structure in a data model. We would name this model user.model.ts
.
export interface Geo {
lat: string;
lng: string;
}
export interface Address {
street: string;
suite: string;
city: string;
zipcode: string;
geo: Geo;
}
export interface Company {
name: string;
catchPhrase: string;
bs: string;
}
export interface User {
id: number;
name: string;
username: string;
email: string;
address: Address;
phone: string;
website: string;
company: Company;
}
Next, use the caching mechanism we previously created to create a UserService
that communicates with this API endpoint.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { User } from '../../models/user.model';
@Injectable({
providedIn: 'root'
})
export class UserService {
private apiUrl = 'https://jsonplaceholder.typicode.com/users'; // API endpoint for user data
constructor(private http: HttpClient) {}
// Fetch all users
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl);
}
// Example method: Search users by username
searchUsers(searchTerm: string): Observable<User[]> {
// Example: Searching by user name using query parameters
return this.http.get<User[]>(`${this.apiUrl}?username=${searchTerm}`);
}
}
Next, include the CachingInterceptor
and UserService
among the providers
array of the app.component.ts
file then import CommonModule
, FormsModule
, and HttpClientModule
to the imports
array too.
@Component({
// other metadata
imports: [
CommonModule,
FormsModule,
HttpClientModule,
// other imports
],
providers: [
UserService,
{
provide: HTTP_INTERCEPTORS,
useClass: CachingInterceptor,
multi: true,
},
],
})
Within the AppComponent
class, load the initial set of user data by calling the loadUsers
method. This would use the UserService
to fetch data from the server.
Add a search
function to check the cache when a search term is entered, and then add checks in this function to look through the cache for the search term. It would print “Cache for [searchTerm] found” if it is there and if it isn’t, it should log “No Cache for [searchTerm] found” to the console.
export class AppComponent implements OnInit {
title = 'caching';
users: User[] = [];
searchTerm: string = '';
constructor(
private userService: UserService,
private cacheService: CacheService
) {}
ngOnInit() {
// Load initial users (optional)
this.loadUsers();
}
// Function to load users
loadUsers() {
this.userService.getUsers().subscribe(
(data) => {
this.users = data;
},
(error) => {
console.error('Error loading users:', error);
}
);
}
// Function to search users based on searchTerm
search() {
if (this.searchTerm.trim() === '') {
// If the search term is empty, load all users
this.loadUsers();
return;
}
// Check cache first
const cachedUsers = this.cacheService.get(this.searchTerm.toLowerCase());
if (cachedUsers) {
console.log(`Cache for ${this.searchTerm} found`);
this.users = cachedUsers;
} else {
console.log(`No Cache for ${this.searchTerm} found`);
// Perform server request
this.userService.searchUsers(this.searchTerm).subscribe(
(data) => {
this.users = data;
// Cache the result
this.cacheService.put(this.searchTerm.toLowerCase(), data);
},
(error) => {
console.error('Error searching users:', error);
}
);
}
}
}
To complete the setup, create an HTML template that includes a search input
field and a corresponding search button. This template also displays all the data loaded from the endpoint.
<div>
<!-- Title -->
<h1>{{ title }}</h1>
<!-- Search Input and Button -->
<input type="text" [(ngModel)]="searchTerm" placeholder="Search by name" />
<button (click)="search()">Search</button>
<!-- Display Searched Name (if searchTerm is not empty) -->
<div *ngIf="searchTerm !== ''">
<p>Searched Name: {{ searchTerm }}</p>
</div>
<!-- User List -->
<ul>
<!-- Iterate over each user in the users' array -->
<li *ngFor="let user of users">
<!-- User Information -->
<strong>{{ user.name }}</strong> ({{ user.username }})
<br />
Email: {{ user.email }}
<br />
Address: {{ user.address.street }}, {{ user.address.suite }}, {{
user.address.city }}, {{ user.address.zipcode }}
<br />
Phone: {{ user.phone }}
<br />
Website:
<a [href]="'http://' + user.website" target="_blank"
>{{ user.website }}</a
>
<br />
Company: {{ user.company.name }} - {{ user.company.catchPhrase }}
</li>
</ul>
</div>
The image below shows the console output for searching a specific name. Initially, it displays “No Cache for the user found.” On subsequent searches, it shows “Cache for the user found,” indicating that the data was cached and retrieved successfully.
The image below shows that the server call is made only once, even if we click the search button multiple times because the information is cached.
In contrast, the image below shows that if we remove the code checking for cached data in the AppComponent
class:
if (cachedUsers) {
console.log(`Cache for ${this.searchTerm} found`);
this.users = cachedUsers;
} else {
console.log(`No Cache for ${this.searchTerm} found`);
}
Searching for the same term multiple times results in repeated server calls. Without caching, the app doesn’t store previous search results locally and fetches the data from the server every time.
Conclusion
Implementing HTTP caching using Angular’s HttpInterceptor
boosts web app performance by storing responses locally. This reduces server load and speeds up load times by serving cached data instead of repeatedly fetching it from the server. Apps where users frequently access the same data can benefit from this since it guarantees higher efficiency and faster answers. Angular’s HttpInterceptor
caching allows developers to use consistent caching techniques across the application. This improves speed and provides a faster and more seamless user experience.
Additional Resources
For further research, you can explore the following: HttpInterceptor.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.