Published on August 5, 2023
Performance is a critical aspect of any web application. A slow, unresponsive application can lead to poor user experience and high bounce rates. Angular provides several built-in features and techniques to optimize performance. This post explores various strategies to improve the performance of your Angular applications, from simple optimizations to advanced techniques.
Before diving into specific optimization techniques, it’s important to understand what affects Angular application performance:
Each of these aspects can be optimized using different techniques, which we’ll explore in this post.
Angular’s change detection mechanism is responsible for keeping the UI in sync with the application state. By default, Angular uses a strategy called “Zone.js-based change detection,” which can be inefficient for large applications.
The OnPush change detection strategy tells Angular to only check for changes when:
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent {
// Component logic
}
For even more control, you can manually detach and reattach the change detector:
import { Component, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.component.html'
})
export class MyComponent {
constructor(private cdr: ChangeDetectorRef) {
// Detach the change detector
this.cdr.detach();
// Some operation that doesn't require UI updates
// Reattach and check for changes when needed
this.cdr.reattach();
// or just detect changes without reattaching
this.cdr.detectChanges();
}
}
Pure pipes are only recalculated when their input values change, making them more efficient than impure pipes or component methods:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'filter',
pure: true // This is the default
})
export class FilterPipe implements PipeTransform {
transform(items: any[], field: string, value: any): any[] {
if (!items) return [];
return items.filter(item => item[field] === value);
}
}
When to use:
Lazy loading is a design pattern that delays the loading of non-essential resources until they are needed. In Angular, this typically means loading feature modules only when the user navigates to their routes.
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', loadChildren: () => import('./home/home.module').then(m => m.HomeModule) },
{ path: 'products', loadChildren: () => import('./products/products.module').then(m => m.ProductsModule) },
{ path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Angular provides several preloading strategies to improve the user experience:
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';
const routes: Routes = [
// ... routes as before
];
@NgModule({
imports: [RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules
})],
exports: [RouterModule]
})
export class AppRoutingModule { }
For more control, you can create a custom preloading strategy:
// selective-preloading-strategy.ts
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class SelectivePreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
return route.data && route.data.preload ? load() : of(null);
}
}
Then use it in your routing module:
// app-routing.module.ts
const routes: Routes = [
{
path: 'products',
loadChildren: () => import('./products/products.module').then(m => m.ProductsModule),
data: { preload: true }
},
// ... other routes
];
@NgModule({
imports: [RouterModule.forRoot(routes, {
preloadingStrategy: SelectivePreloadingStrategy
})],
exports: [RouterModule]
})
export class AppRoutingModule { }
When to use:
Ahead-of-Time (AOT) compilation compiles your Angular application during the build process rather than at runtime in the browser.
AOT compilation is enabled by default in production builds with Angular CLI:
ng build --prod
For development builds, you can enable it with:
ng build --aot
When to use:
Tree shaking is a build optimization technique that removes unused code from the final bundle.
Tree shaking works by analyzing the import and export statements in your code and removing any code that isn’t actually used.
Tree shaking is enabled by default in production builds with Angular CLI. To make the most of it:
"sideEffects": false
flag in your package.json (or specify files with side effects)// package.json
{
"name": "my-app",
"version": "0.0.0",
"sideEffects": false,
// or
"sideEffects": [
"*.css",
"*.scss"
]
}
When to use:
Optimizing your application’s bundle size is crucial for improving initial load time.
Code splitting divides your code into smaller chunks that can be loaded on demand:
// Using dynamic imports for code splitting
const loadLibrary = async () => {
const library = await import('./heavy-library');
return library;
};
// Use the library only when needed
button.addEventListener('click', async () => {
const library = await loadLibrary();
library.doSomething();
});
Use tools like Webpack Bundle Analyzer to identify large dependencies:
ng build --prod --stats-json
npx webpack-bundle-analyzer dist/my-app/stats.json
Angular CLI automatically creates differential builds for modern and legacy browsers:
// tsconfig.json
{
"compilerOptions": {
"target": "es2015",
// ...
}
}
When to use:
Proper memory management is essential for maintaining good runtime performance.
The most common cause of memory leaks in Angular applications is forgetting to unsubscribe from Observables:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { DataService } from './data.service';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.component.html'
})
export class MyComponent implements OnInit, OnDestroy {
private subscription = new Subscription();
constructor(private dataService: DataService) {}
ngOnInit() {
this.subscription.add(
this.dataService.getData().subscribe(data => {
// Handle data
})
);
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
The async pipe automatically subscribes and unsubscribes from Observables:
<div *ngIf="data$ | async as data">
</div>
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { DataService } from './data.service';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.component.html'
})
export class MyComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
constructor(private dataService: DataService) {}
ngOnInit() {
this.dataService.getData().pipe(
takeUntil(this.destroy$)
).subscribe(data => {
// Handle data
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
When to use:
Server-Side Rendering (SSR) generates the initial HTML for your application on the server rather than in the browser.
ng add @nguniversal/express-engine
This command adds the necessary dependencies and configuration for SSR with Express.
npm run build:ssr
npm run serve:ssr
When to use:
Web Workers allow you to run CPU-intensive tasks in a background thread, keeping the main thread free for UI updates.
ng generate web-worker app
This command generates a web worker and updates your application to use it.
// app.component.ts
export class AppComponent {
constructor() {
if (typeof Worker !== 'undefined') {
// Create a new web worker
const worker = new Worker('./app.worker', { type: 'module' });
// Send data to the worker
worker.postMessage({ data: 'hello' });
// Receive data from the worker
worker.onmessage = ({ data }) => {
console.log(`Received from worker: ${data}`);
};
} else {
// Web Workers are not supported in this environment
// You should add a fallback so that your program still executes correctly
}
}
}
// app.worker.ts
/// <reference lib="webworker" />
addEventListener('message', ({ data }) => {
// Perform CPU-intensive task
const result = performHeavyCalculation(data);
// Send result back to the main thread
postMessage(result);
});
function performHeavyCalculation(data) {
// Your CPU-intensive code here
return `Processed: ${data.data}`;
}
When to use:
Virtual scrolling renders only the items currently visible in the viewport, improving performance for long lists.
First, install the CDK:
ng add @angular/cdk
Then import the ScrollingModule:
// app.module.ts
import { ScrollingModule } from '@angular/cdk/scrolling';
@NgModule({
imports: [
// ... other imports
ScrollingModule
],
// ... rest of the module
})
export class AppModule { }
<!-- virtual-scroll.component.html -->
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
<div *cdkVirtualFor="let item of items" class="item">
</div>
</cdk-virtual-scroll-viewport>
// virtual-scroll.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-virtual-scroll',
templateUrl: './virtual-scroll.component.html',
styleUrls: ['./virtual-scroll.component.scss']
})
export class VirtualScrollComponent {
items = Array.from({length: 100000}).map((_, i) => `Item #${i}`);
}
/* virtual-scroll.component.scss */
.viewport {
height: 400px;
width: 100%;
border: 1px solid black;
}
.item {
height: 50px;
padding: 10px;
border-bottom: 1px solid #ccc;
}
When to use:
Identifying performance bottlenecks is the first step in optimization.
Angular DevTools is a Chrome extension that provides Angular-specific debugging and profiling tools:
Lighthouse is an automated tool for improving web page quality:
When to use:
ng build --prod
Enable Gzip Compression: Configure your server to use Gzip compression for static assets
Implement Caching Strategies: Use appropriate HTTP caching headers for your assets
Optimize Images: Use modern image formats (WebP) and responsive images
Use OnPush Change Detection: Apply OnPush change detection to components that don’t need frequent updates
Avoid Complex Expressions in Templates: Move complex logic to component methods or pure pipes
<div *ngFor="let item of items; trackBy: trackByFn"></div>
trackByFn(index: number, item: any): number {
return item.id;
}
Lazy Load Non-Critical Resources: Use dynamic imports for libraries not needed immediately
Optimize NgRx Usage: Use entity adapters and memoized selectors for efficient state management
Avoid Unnecessary Re-renders: Be careful with object and array references to prevent unnecessary change detection
Optimizing Angular applications requires a multi-faceted approach, addressing everything from initial load time to runtime performance and memory management. By implementing the techniques discussed in this post, you can significantly improve your application’s performance and provide a better user experience.
Remember that performance optimization is an ongoing process. Regularly profile your application, identify bottlenecks, and apply the appropriate optimizations. Start with the simplest optimizations that provide the biggest impact, such as enabling production builds and AOT compilation, before moving on to more complex techniques like SSR or Web Workers.
By following these best practices and techniques, you can build Angular applications that are not only feature-rich but also fast and responsive.