All posts

Angular performance optimization: Strategies and tools 🚀

Introduction: Performance Is a Feature

In a market where 53% of mobile users abandon a page that takes more than 3 seconds to load (source: Google/SOASTA), performance is not an optional optimization—it is a product requirement. For Angular applications, this translates into precise architectural choices, built-in profiling tools, and intelligent rendering strategies.

In this guide, we explore the most effective techniques to optimize an Angular application—from compilation to bundling, from change detection to HTTP caching— with real code examples and reference metrics.

Topics: #Angular #Performance #LazyLoading #ChangeDetection #WebDevelopment

Section 1: AOT Compilation and Optimized Build

Ahead-of-Time (AOT) Compilation

Angular compiles HTML templates into JavaScript during the build phase, not at runtime in the browser. Since Angular 9, AOT compilation is enabled by default in production and provides three measurable benefits:

  • Smaller bundle: the Angular compiler (@angular/compiler) is not included in the final bundle.
  • Faster rendering: the browser receives precompiled JavaScript, not templates to interpret.
  • Early error detection: invalid bindings and type mismatches are caught at build time.
# Production build with AOT (enabled by default)
ng build --configuration production

# Verify AOT is enabled
# angular.json → projects → [name] → architect → build → configurations → production
# "aot": true

Tree Shaking and Bundle Analysis

Tree shaking is the process where the bundler (esbuild in Angular 17+) removes unused exported code. The result is a significantly smaller final bundle.

// ❌ Importing entire module — prevents tree shaking
import * as _ from 'lodash';
const result = _.chunk([1, 2, 3, 4], 2);

// ✅ Specific import — tree-shakeable
import chunk from 'lodash/chunk';
const result = chunk([1, 2, 3, 4], 2);

// ✅ Even better: use native JS
const result = [[1, 2], [3, 4]];

Visual Bundle Analysis

Before optimizing, measure. The webpack-bundle-analyzer plugin generates an interactive map of your bundle.

npm install --save-dev webpack-bundle-analyzer source-map-explorer

ng build --configuration production --source-map
npx source-map-explorer dist/my-app/browser/*.js

Section 2: Lazy Loading — Load Only What You Need

Route-Level Lazy Loading

Lazy loading splits the application into separate chunks. The browser downloads only the current route’s chunk, reducing Time To Interactive (TTI).

// app.routes.ts
import { Routes } from '@angular/router';

export const APP_ROUTES: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./features/home/home.component').then(c => c.HomeComponent),
  },
  {
    path: 'admin',
    loadChildren: () =>
      import('./features/admin/admin.routes').then(m => m.ADMIN_ROUTES),
  },
  {
    path: 'blog',
    loadChildren: () =>
      import('./features/blog/blog.routes').then(m => m.BLOG_ROUTES),
  },
];

Preloading Strategy

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
import { APP_ROUTES } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(APP_ROUTES, withPreloading(PreloadAllModules)),
  ],
};

Section 3: Change Detection — Angular’s Performance Nerve Center

OnPush Strategy

// blog-card.component.ts
import {
  Component, Input, ChangeDetectionStrategy
} from '@angular/core';

@Component({
  selector: 'app-blog-card',
  standalone: true,
  templateUrl: './blog-card.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BlogCardComponent {
  @Input({ required: true }) post!: any;
}

// ❌ Mutation (not detected)
this.post.title = 'New Title';

// ✅ New reference (detected)
this.post = { ...this.post, title: 'New Title' };

Angular Signals

// counter.component.ts
import { Component, signal, computed, effect } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Count: {{ count() }}</p>
    <p>Double: {{ doubled() }}</p>
    <button (click)="increment()">+1</button>
  `,
})
export class CounterComponent {
  readonly count = signal(0);
  readonly doubled = computed(() => this.count() * 2);

  constructor() {
    effect(() => console.log('Updated:', this.count()));
  }

  increment() {
    this.count.update(n => n + 1);
  }
}

Section 4: HTTP Optimization

HTTP Cache with Interceptor

// cache.interceptor.ts
import { HttpInterceptorFn, HttpResponse } from '@angular/common/http';
import { of } from 'rxjs';
import { tap } from 'rxjs/operators';

const cache = new Map<string, any>();
const TTL = 5 * 60 * 1000;

export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
  if (req.method !== 'GET') return next(req);

  const cached = cache.get(req.url);
  if (cached && Date.now() - cached.time < TTL) {
    return of(cached.response);
  }

  return next(req).pipe(
    tap(event => {
      if (event instanceof HttpResponse) {
        cache.set(req.url, { response: event, time: Date.now() });
      }
    })
  );
};

Section 5: Image Optimization

NgOptimizedImage

// component.ts
import { NgOptimizedImage } from '@angular/common';

@Component({
  standalone: true,
  imports: [NgOptimizedImage],
  template: `
    <img
      ngSrc="/assets/hero.webp"
      width="1200"
      height="600"
      priority
      alt="Hero"
    />
  `,
})

Image Compression

cwebp -q 80 input.jpg -o output.webp
avifenc --quality 60 input.jpg output.avif
npx svgo --input assets/icons --recursive

Section 6: Third-Party Dependencies

// ❌ Heavy dependency
import moment from 'moment';

// ✅ Native alternative
const formatted = new Intl.DateTimeFormat('en-US').format(new Date());

// ✅ Lightweight alternative
import { format } from 'date-fns';
const formatted = format(new Date(), 'dd/MM/yyyy');

Section 7: Profiling Tools

Lighthouse CLI

npm install -g lighthouse

lighthouse https://your-site.com \
  --output html \
  --output-path ./report.html

Summary: Angular Optimization Checklist

  • ✅ Production build with AOT
  • ✅ Lazy loading enabled
  • ✅ OnPush strategy used
  • ✅ trackBy in lists
  • ✅ HTTP caching
  • ✅ Optimized images
  • ✅ Minimal dependencies

Conclusion

Optimizing an Angular application is not a one-time tweak but an ongoing discipline. It blends architecture, coding practices, and continuous measurement.

Always start by measuring: Lighthouse and profiling tools will reveal the real bottlenecks, helping you avoid premature optimizations.

#Angular #Performance #FrontendDevelopment