Why Performance Matters
Fast applications provide better user experiences, higher engagement, and better SEO rankings. Angular provides many built-in tools for optimization.
Change Detection Strategies
Default vs OnPush
Angular's default change detection checks every component when any event occurs. OnPush limits checks to only when inputs change or events fire within the component.
// DEFAULT: Checked every change detection cycle (less performant)
@Component({ ... })
export class DefaultComponent {
items: Item[] = [];
}
// ONPUSH: Only checked when @Input() changes or event occurs (more performant)
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@for (item of items; track item.id) {
<app-item-card [item]="item" />
}
`,
})
export class OptimizedComponent {
@Input() items: Item[] = [];
}OnPush Rules
For OnPush to work correctly:
| Triggers Re-render | Does NOT Trigger |
|---|---|
@Input() reference change | Mutating an object/array |
| DOM events in the component | Parent change detection (unless input changed) |
async pipe emission | setTimeout / setInterval (external) |
| Signals changing | Direct property changes from service subscriptions |
Manual markForCheck() | — |
Immutable Updates with OnPush
// ❌ WRONG — Mutation won't trigger OnPush
this.items.push(newItem);
// ✅ CORRECT — New array reference
this.items = [...this.items, newItem];
// ✅ CORRECT — New object reference
this.user = { ...this.user, name: 'Updated' };TrackBy in Loops
When rendering lists, always use track to help Angular identify items:
<!-- ❌ Without track: entire list re-renders on change -->
@for (user of users; track $index) {
<app-user-card [user]="user" />
}
<!-- ✅ With unique track: only changed items re-render -->
@for (user of users; track user.id) {
<app-user-card [user]="user" />
}Lazy Loading
Route-Level Lazy Loading
// app.routes.ts
export const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'admin',
loadComponent: () =>
import('./admin/admin.component').then(m => m.AdminComponent),
},
{
path: 'dashboard',
loadChildren: () =>
import('./dashboard/dashboard.routes').then(m => m.DASHBOARD_ROUTES),
},
];Defer Loading (Angular 17+)
Load components lazily based on triggers:
<!-- Load when visible in viewport -->
@defer (on viewport) {
<app-heavy-chart [data]="chartData" />
} @placeholder {
<div class="chart-skeleton">Loading chart...</div>
} @loading (minimum 500ms) {
<app-spinner />
}
<!-- Load on interaction -->
@defer (on interaction) {
<app-comments [postId]="post.id" />
} @placeholder {
<button>Load Comments</button>
}
<!-- Load when idle -->
@defer (on idle) {
<app-recommendations />
}
<!-- Load on hover -->
@defer (on hover) {
<app-tooltip [text]="helpText" />
} @placeholder {
<span>ℹ️</span>
}Image Optimization
NgOptimizedImage
import { NgOptimizedImage } from '@angular/common';
@Component({
imports: [NgOptimizedImage],
template: `
<!-- Prioritize above-the-fold images -->
<img ngSrc="/hero.jpg" width="1200" height="600" priority>
<!-- Lazy load below-the-fold images (default) -->
<img ngSrc="/product.jpg" width="400" height="300" placeholder>
<!-- Responsive images -->
<img ngSrc="/photo.jpg"
width="800" height="600"
sizes="(max-width: 768px) 100vw, 50vw"
[ngSrcset]="'200w, 400w, 800w, 1200w'">
`,
})
export class ImageComponent {}Virtual Scrolling
For large lists, render only visible items:
import { ScrollingModule } from '@angular/cdk/scrolling';
@Component({
imports: [ScrollingModule],
template: `
<cdk-virtual-scroll-viewport itemSize="72" class="list-viewport">
<div *cdkVirtualFor="let item of items; trackBy: trackById" class="list-item">
{{ item.name }}
</div>
</cdk-virtual-scroll-viewport>
`,
styles: [`
.list-viewport { height: 500px; }
.list-item { height: 72px; }
`],
})
export class LargeListComponent {
items = Array.from({ length: 100000 }, (_, i) => ({ id: i, name: `Item ${i}` }));
trackById = (_: number, item: { id: number }) => item.id;
}Bundle Optimization
Analyze Bundle Size
# Install bundle analyzer
npm install --save-dev webpack-bundle-analyzer
# Build with stats
ng build --stats-json
# Analyze
npx webpack-bundle-analyzer dist/my-app/stats.jsonTree Shaking Tips
// ✅ Import only what you need
import { MatButtonModule } from '@angular/material/button';
// ❌ Don't import entire libraries
import * as _ from 'lodash';
// ✅ Import specific functions
import { debounce } from 'lodash-es/debounce';Preloading Strategies
// Preload all lazy modules after initial load
import { PreloadAllModules } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withPreloading(PreloadAllModules)),
],
};Runtime Optimizations
Memoize Expensive Computations
// ❌ Function called every change detection cycle
@Component({
template: `<p>{{ getExpensiveValue() }}</p>`,
})
// ✅ Use computed signals or pipes for memoization
@Component({
template: `<p>{{ expensiveValue() }}</p>`,
})
export class OptimizedComponent {
data = signal(rawData);
expensiveValue = computed(() => {
return this.data().reduce((sum, item) => sum + item.calculate(), 0);
});
}Pure Pipes for Template Computations
// Pure pipe only recalculates when input changes
@Pipe({ name: 'filterActive', pure: true, standalone: true })
export class FilterActivePipe implements PipeTransform {
transform(items: User[]): User[] {
return items.filter(item => item.active);
}
}<!-- Efficient: pipe only recalculates when users reference changes -->
@for (user of users | filterActive; track user.id) {
<app-user-card [user]="user" />
}Detach Change Detection
For rarely-updated views:
export class StaticComponent {
private cdr = inject(ChangeDetectorRef);
ngOnInit() {
this.cdr.detach(); // Stop automatic checks
// Manually check when needed
setInterval(() => {
this.cdr.detectChanges();
}, 5000); // Update every 5 seconds
}
}Web Workers
Offload CPU-intensive work:
ng generate web-worker heavy-task// heavy-task.worker.ts
addEventListener('message', ({ data }) => {
const result = performHeavyComputation(data);
postMessage(result);
});
// component
export class DataComponent {
processData(data: number[]) {
const worker = new Worker(new URL('./heavy-task.worker', import.meta.url));
worker.onmessage = ({ data }) => {
this.result = data;
};
worker.postMessage(data);
}
}Performance Checklist
| Technique | Impact | Effort |
|---|---|---|
| OnPush change detection | High | Medium |
track in loops | High | Low |
| Lazy loading routes | High | Low |
@defer blocks | High | Low |
| Virtual scrolling | High | Medium |
| NgOptimizedImage | Medium | Low |
| Pure pipes | Medium | Low |
| Bundle analysis | Variable | Low |
| Web Workers | High | High |
Next Steps
Your application is optimized! The final topic covers deploying your Angular application — building for production, deployment platforms, and CI/CD pipelines.