Angular Performance Optimization: A Complete Guide
Learn advanced techniques to optimize Angular applications, from change detection to lazy loading and bundle optimization.
- Angular
- Performance
- Optimization
- TypeScript
- RxJS
Performance optimization in Angular requires understanding change detection, rendering strategies, and bundle management. After optimizing multiple production applications handling complex data flows, here's what actually works in Angular applications.
Change Detection Strategy
The most impactful optimization is using OnPush change detection:
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
@Component({
selector: 'app-user-card',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div>
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
`,
})
export class UserCardComponent {
@Input() user!: User;
// Only re-renders when @Input() reference changes
}
Signals for Performance
Signals provide automatic optimization with OnPush:
import { Component, signal, computed, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-product-list',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div *ngFor="let product of filteredProducts()">
{{ product.name }}
</div>
`,
})
export class ProductListComponent {
products = signal<Product[]>([]);
filter = signal<string>('');
// Only recomputes when dependencies change
filteredProducts = computed(() => {
const filterValue = this.filter().toLowerCase();
return this.products().filter(product =>
product.name.toLowerCase().includes(filterValue)
);
});
}
Lazy Loading Modules
Lazy load feature modules to reduce initial bundle size:
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule),
},
{
path: 'settings',
loadComponent: () => import('./settings/settings.component').then(c => c.SettingsComponent),
},
];
TrackBy Functions
Use trackBy to optimize *ngFor:
// Old way (recreates all DOM nodes)
<div *ngFor="let item of items">{{ item.name }}</div>
// Optimized way
@Component({
template: `
<div *ngFor="let item of items; trackBy: trackByFn">
{{ item.name }}
</div>
`,
})
export class ListComponent {
items: Item[] = [];
trackByFn(index: number, item: Item): string {
return item.id; // Stable identifier
}
}
// With new @for syntax (trackBy built-in)
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
}
OnPush with Immutable Data
Ensure OnPush works correctly with immutable updates:
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
@Component({
selector: 'app-todo-list',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div *ngFor="let todo of todos">
{{ todo.text }}
</div>
`,
})
export class TodoListComponent {
@Input() todos: Todo[] = [];
addTodo(text: string): void {
// Create new array reference
this.todos = [...this.todos, { id: Date.now(), text, done: false }];
}
}
Virtual Scrolling
Use CDK Virtual Scrolling for large lists:
import { ScrollingModule } from '@angular/cdk/scrolling';
@Component({
selector: 'app-large-list',
imports: [ScrollingModule],
template: `
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
<div *cdkVirtualFor="let item of items; trackBy: trackByFn">
{{ item.name }}
</div>
</cdk-virtual-scroll-viewport>
`,
styles: [`
.viewport {
height: 600px;
}
`],
})
export class LargeListComponent {
items: Item[] = [];
trackByFn(index: number, item: Item): string {
return item.id;
}
}
RxJS Optimization
Optimize RxJS subscriptions:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject, takeUntil, debounceTime, distinctUntilChanged } from 'rxjs';
@Component({
selector: 'app-search',
template: `
<input (input)="searchTerm$.next($event.target.value)" />
<div *ngFor="let result of results">{{ result }}</div>
`,
})
export class SearchComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
searchTerm$ = new Subject<string>();
results: string[] = [];
constructor(private searchService: SearchService) {}
ngOnInit(): void {
this.searchTerm$.pipe(
debounceTime(300),
distinctUntilChanged(),
takeUntil(this.destroy$)
).subscribe(term => {
this.searchService.search(term).subscribe(results => {
this.results = results;
});
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
Async Pipe Optimization
Use async pipe for automatic subscription management:
@Component({
template: `
<div *ngIf="users$ | async as users">
<div *ngFor="let user of users">{{ user.name }}</div>
</div>
`,
})
export class UserListComponent {
users$ = this.userService.getUsers();
// Async pipe automatically subscribes/unsubscribes
}
Image Optimization
Lazy load images and use modern formats:
@Component({
selector: 'app-image',
template: `
<img
[src]="imageSrc"
[alt]="alt"
loading="lazy"
(load)="onImageLoad()"
/>
`,
})
export class ImageComponent {
@Input() imageSrc!: string;
@Input() alt!: string;
loaded = signal<boolean>(false);
onImageLoad(): void {
this.loaded.set(true);
}
}
Bundle Optimization
Reduce bundle size:
// Use tree-shakeable imports
import { debounceTime } from 'rxjs/operators';
// Instead of: import { debounceTime } from 'rxjs';
// Use providedIn: 'root' for services
@Injectable({ providedIn: 'root' })
export class UserService {
// Tree-shakeable if unused
}
// Lazy load heavy libraries
async loadChartLibrary(): Promise<void> {
const { Chart } = await import('chart.js');
// Use Chart
}
Change Detection Optimization
Detach change detection when appropriate:
import { Component, ChangeDetectorRef, OnDestroy } from '@angular/core';
@Component({
selector: 'app-heavy-component',
template: `<div>{{ data }}</div>`,
})
export class HeavyComponent implements OnDestroy {
data: string = '';
constructor(private cdr: ChangeDetectorRef) {
// Detach for manual control
this.cdr.detach();
}
updateData(newData: string): void {
this.data = newData;
this.cdr.detectChanges(); // Manual change detection
}
ngOnDestroy(): void {
this.cdr.reattach(); // Reattach before destroy
}
}
Memory Leak Prevention
Prevent memory leaks:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-subscription-manager',
template: `<div>{{ data }}</div>`,
})
export class SubscriptionManagerComponent implements OnInit, OnDestroy {
private subscriptions = new Subscription();
data: string = '';
ngOnInit(): void {
const sub1 = this.service.getData().subscribe(data => {
this.data = data;
});
const sub2 = this.anotherService.getData().subscribe(data => {
// Handle data
});
this.subscriptions.add(sub1);
this.subscriptions.add(sub2);
}
ngOnDestroy(): void {
this.subscriptions.unsubscribe(); // Clean up all subscriptions
}
}
Performance Monitoring
Monitor performance with Angular DevTools:
import { Component, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-monitored',
template: `<div>Content</div>`,
})
export class MonitoredComponent implements AfterViewInit {
ngAfterViewInit(): void {
// Use Angular DevTools Profiler
// Or custom performance monitoring
performance.mark('component-rendered');
}
}
Best Practices Summary
- Use OnPush: Default to OnPush change detection
- Adopt Signals: Use signals for reactive state
- Lazy load routes: Reduce initial bundle size
- Use trackBy: Optimize *ngFor rendering
- Virtual scrolling: For lists with 100+ items
- Optimize RxJS: Use operators efficiently
- Async pipe: Automatic subscription management
- Tree shaking: Use tree-shakeable imports
- Prevent leaks: Always unsubscribe
- Monitor performance: Use profiling tools
Real-World Results
In production applications, these optimizations have resulted in:
- 50-70% reduction in initial bundle size
- 60-80% improvement in Time to Interactive
- 40-60% reduction in memory usage
- Smooth 60fps interactions
- Better mobile performance
Real-World Example
Complete optimized component:
import { Component, ChangeDetectionStrategy, signal, computed, input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ScrollingModule } from '@angular/cdk/scrolling';
interface Player {
id: string;
name: string;
position: string;
}
@Component({
selector: 'app-player-list',
standalone: true,
imports: [CommonModule, ScrollingModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<input
[(ngModel)]="searchTerm"
(ngModelChange)="searchTerm.set($event)"
placeholder="Search players"
/>
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
@for (player of filteredPlayers(); track player.id) {
<div class="player-item">
{{ player.name }} - {{ player.position }}
</div>
}
</cdk-virtual-scroll-viewport>
`,
styles: [`
.viewport {
height: 600px;
}
`],
})
export class PlayerListComponent {
players = input.required<Player[]>();
searchTerm = signal<string>('');
filteredPlayers = computed(() => {
const term = this.searchTerm().toLowerCase();
return this.players().filter(player =>
player.name.toLowerCase().includes(term)
);
});
}
Angular performance optimization requires a combination of strategies. Start with OnPush and Signals, then optimize based on profiling results. The key is measuring first, then optimizing where it matters most.
Keep reading
Performance Optimization in React: A Practical Guide
Learn practical techniques to optimize React applications, from code splitting to memoization strategies and rendering optimizations.
NgRx State Management: Enterprise-Ready Angular Architecture
Master NgRx for scalable Angular applications. Learn actions, reducers, effects, and selectors to build maintainable state management solutions.
RxJS Essentials: Most Used Operators and Patterns
Master the most commonly used RxJS operators and patterns for handling async operations, data transformation, and reactive programming in Angular.