All Course > Angular > Angular Performance Optimization Feb 16, 2025

Angular Caching & Lazy Loading: Speed Up Your App

In the previous lesson, we covered performance tuning in Angular apps by reducing bundle size, using AOT compilation, and tree shaking. Now, we’ll dive into caching and lazy loading, which help cut API calls and make your app faster.

I once built an e-commerce app that loaded slowly because it fetched the same product data repeatedly. Users got frustrated, and bounce rates shot up. That’s when I realized I needed caching to store data locally and lazy loading to load features only when needed.

Caching saves data on the user’s device, so the app doesn’t keep calling the server. Lazy loading splits the app into chunks, loading only the parts the user needs. Together, they make your app faster and smoother.

Caching API Responses with LocalStorage

When building fast, responsive Angular apps, reducing unnecessary API calls is key. One of the simplest ways to do this is by caching responses in LocalStorage.

I once worked on a dashboard app that fetched user data on every page reload. This made the app slow, especially on weak networks. To fix this, I stored the API response in LocalStorage, so the next time the user visited, the data loaded instantly.

LocalStorage is a browser storage that:
- Stores data as key-value pairs (up to 5-10MB, depending on the browser).
- Persists even after the user closes the browser.
- Works synchronously, meaning it doesn’t slow down your app with async calls.

When Should You Cache Data?
- Static data (e.g., product listings, user profiles).
- Frequently accessed data (e.g., user preferences).
- Real-time data (e.g., live chat, stock prices).

Implementing LocalStorage Caching

Create a Cache Service

First, I built a reusable service to handle caching logic.

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class CacheService {
  // Get data from cache
  get(key: string): any {
    const data = localStorage.getItem(key);
    return data ? JSON.parse(data) : null;
  }

  // Save data to cache
  set(key: string, data: any): void {
    localStorage.setItem(key, JSON.stringify(data));
  }

  // Clear a specific cache entry
  remove(key: string): void {
    localStorage.removeItem(key);
  }

  // Clear all cached data (optional)
  clear(): void {
    localStorage.clear();
  }
}

Use the Cache in an API Service

Next, I modified my data-fetching service to check the cache before making an API call.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { CacheService } from './cache.service';

@Injectable({ providedIn: 'root' })
export class ProductService {
  private apiUrl = 'https://api.example.com/products';

  constructor(private http: HttpClient, private cache: CacheService) {}

  getProducts() {
    const cacheKey = 'products_data';
    const cachedData = this.cache.get(cacheKey);

    // Return cached data if available
    if (cachedData) {
      console.log('Loading from cache...');
      return of(cachedData); // Using RxJS 'of' to return as Observable
    }

    // Else, fetch from API and store in cache
    return this.http.get(this.apiUrl).pipe(
      tap((data) => {
        this.cache.set(cacheKey, data);
        console.log('Storing in cache...');
      })
    );
  }
}

Set an Expiry Time for Cached Data (Optional)

Sometimes, cached data becomes outdated. To fix this, I added an expiry check.

// Updated CacheService method
setWithExpiry(key: string, data: any, expiryInMinutes: number): void {
  const now = new Date();
  const item = {
    data,
    expiry: now.getTime() + expiryInMinutes * 60000, // Convert to milliseconds
  };
  localStorage.setItem(key, JSON.stringify(item));
}

getWithExpiry(key: string): any {
  const itemStr = localStorage.getItem(key);
  if (!itemStr) return null;

  const item = JSON.parse(itemStr);
  const now = new Date();

  if (now.getTime() > item.expiry) {
    localStorage.removeItem(key); // Clear expired data
    return null;
  }

  return item.data;
}

Clear Cache When Needed

If the user logs out or data changes, I clear the cache:

// Example: Clear cache after user logout
logout() {
  this.cache.remove('user_profile');
  this.cache.remove('products_data');
}

When NOT to Use LocalStorage Caching

  • Sensitive data (LocalStorage is not secure—use HTTP-only cookies instead).
  • Large datasets (Storing too much can slow down the browser).
  • Frequently changing data (e.g., live notifications).

Using Service Workers for Offline Caching in Angular

Service workers are a powerful tool that lets your Angular app work offline by caching key assets and API responses. They run in the background, separate from your main app, intercepting network requests to serve cached content when needed.

Why Use Service Workers?

I once worked on a news app where users lost access to articles when their internet dropped. This led to poor user experience and lower engagement. After adding a service worker, the app loaded cached news even offline, keeping users happy.

Service workers help:

  • Cache static files (HTML, CSS, JS) for instant loads.
  • Store API responses to avoid repeated network calls.
  • Enable offline access, improving reliability.

How to Add a Service Worker in Angular

Angular makes it easy with @angular/pwa. Here’s how I set it up:

Install the PWA Package

ng add @angular/pwa --project your-project-name

This:
* Adds @angular/service-worker
* Creates ngsw-config.json (cache rules)
* Updates angular.json & app.module.ts

Configure Caching Rules

Edit ngsw-config.json to decide what to cache:

{
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "resources": {
        "files": ["/favicon.ico", "/index.html", "/*.css", "/*.js"]
      }
    }
  ],
  "dataGroups": [
    {
      "name": "api-cache",
      "urls": ["/api/news", "/api/products"],
      "cacheConfig": {
        "maxSize": 10,
        "maxAge": "1h",
        "strategy": "freshness" // Prioritizes network, falls back to cache
      }
    }
  ]
}
  • assetGroups → Caches static files.
  • dataGroups → Caches API calls (e.g., /api/news).

Verify It Works

Build for production (Service workers don’t work in dev mode):

ng build --prod

Serve locally and check caching:

http-server -p 8080 -c-1 dist/your-project-name

Open DevTools → Application → Service Workers to see if it’s active.
Go offline (Chrome DevTools → Network → Offline) and reload. The app should still load!

Real-World Example: Caching API Data

In my news app, I cached the latest headlines for 1 hour:

"dataGroups": [
  {
    "name": "news-api",
    "urls": ["/api/latest-news"],
    "cacheConfig": {
      "maxSize": 20,
      "maxAge": "1h",
      "strategy": "performance" // Serves cached first, updates in background
    }
  }
]

Result:
- First load → Fetches from the network.
- Next loads → Instantly shows cached data, then silently updates.
- Offline → Still displays the last cached news.

Common Pitfalls & Fixes

Service worker not updating?
- Clear storage (DevTools → Application → Clear storage).
- Use SwUpdate to force updates:

export class AppComponent {
  constructor(private swUpdate: SwUpdate) {
    if (this.swUpdate.isEnabled) {
      this.swUpdate.available.subscribe(() => {
        if (confirm("New version available! Load it?")) {
          window.location.reload();
        }
      });
    }
  }
}

Lazy Loading Routes in Angular

In Angular, lazy loading is a technique that loads feature modules only when needed, rather than all at once when the app starts. This reduces initial load time, saves bandwidth, and improves user experience—especially for large apps with many routes.

Why Use Lazy Loading?

When I built a dashboard app with multiple sections (admin panel, reports, settings), the initial bundle was over 5MB, making the app slow to load. By implementing lazy loading, I split the app into smaller chunks, cutting the first load to under 1MB.

How Lazy Loading Works

  • The main bundle loads first (core Angular + shared modules).
  • Other modules (like /admin or /reports) load only when the user visits those routes.
  • This avoids loading unnecessary code upfront.

Step-by-Step Implementation

Set Up Lazy-Loaded Routes

Instead of importing all modules in AppModule, we load them dynamically using loadChildren.

Before (Eager Loading):

// app.module.ts (Slow - loads everything upfront)  
import { AdminModule } from './admin/admin.module';  
import { ReportsModule } from './reports/reports.module';  

@NgModule({  
  imports: [AdminModule, ReportsModule],  
})  
export class AppModule {}  

After (Lazy Loading):

// app-routing.module.ts (Faster - loads on demand)  
const routes: Routes = [  
  {  
    path: 'admin',  
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),  
  },  
  {  
    path: 'reports',  
    loadChildren: () => import('./reports/reports.module').then(m => m.ReportsModule),  
  },  
];  

@NgModule({  
  imports: [RouterModule.forRoot(routes)],  
  exports: [RouterModule],  
})  
export class AppRoutingModule {}  

Verify Lazy Loading in DevTools

  • Open Chrome DevTools (F12 → Network tab).
  • Refresh the app. Only the main bundle (main.js) loads.
  • Navigate to /admin or /reports. You’ll see a new chunk (e.g., admin-module.js) load dynamically.

Preloading Strategy (Optional)

To balance speed and usability, Angular allows preloading lazy modules in the background after the app starts:

// app-routing.module.ts  
import { PreloadAllModules } from '@angular/router';  

@NgModule({  
  imports: [  
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }),  
  ],  
})  
export class AppRoutingModule {}  
  • Pros: Faster navigation between routes.
  • Cons: Slightly higher initial bandwidth usage.

Common Pitfalls & Fixes

Circular Dependency Errors

If ModuleA imports ModuleB and vice versa, lazy loading breaks.
Fix: Move shared components/dependencies to a SharedModule.

Route Guards & Lazy Loading

If a route has a canLoad guard, the module won’t load until the guard allows it.

{  
  path: 'admin',  
  loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),  
  canLoad: [AuthGuard], // Blocks loading if user isn’t authenticated  
}  

Chunk File Naming (For Better Debugging)

By default, chunks have random names (1.js, 2.js). To make them readable:

// angular.json  
"projects": {  
  "your-app": {  
    "architect": {  
      "build": {  
        "options": {  
          "namedChunks": true // Generates names like 'admin-module.js'  
        }  
      }  
    }  
  }  
}  

Conclusion

By using caching (like LocalStorage and service workers) and lazy loading, you can make your Angular app load faster and reduce unnecessary API calls. These techniques help improve user experience and lower server load, which I’ve seen cut load times by 50% or more in real projects. Now that your app is optimized for speed, the next step is making sure it’s reliable and bug-free. In the next lesson, we’ll dive into unit testing with Jasmine and Karma, where you’ll learn how to write tests that keep your app running smoothly. Ready to take your Angular skills further? Let’s keep building!

Comments

There are no comments yet.

Write a comment

You can use the Markdown syntax to format your comment.

Tags: angular material nested menu