Back

Optimizing Angular Performance with HttpInterceptor Caching

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.

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:

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) and value(any data). In the cache map, save the value under the key by using this.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 uses this.cache.get(key) to retrieve the corresponding value from the cache Map, but if no item matches the key given, the method returns undefined.
get(key: string): any {
  return this.cache.get(key);
}
  • The clear Method The clear method removes all items from the cache by calling the this.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.

test (1)

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.

test (2)

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`);
}

cache

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.

OpenReplay