Performance
6 min read- Angular Performance Optimization
- Change Detection
- OnPush Change Detection Strategy
- Detaching Change Detection
- TrackBy for ngFor
- Signals vs Observables (performance + architecture)
- 1) Signals are synchronous & fine-grained; Observables are async & push-based
- 2) Signals can reduce change detection cost
- 3) Use Observables for I/O and async flows; Signals for local state
- 4) Signals eliminate subscription management for local state
- 5) Computed signals are memoized (hidden performance win)
- Follow-up interview question: Should signals replace observables?
- Performance pitfalls interviewers love (and fixes)
- 1) Default change detection everywhere
- 2) Overusing Observables for local UI state
- 3) `*ngFor` without `trackBy`
- 4) Heavy logic in templates
- 5) `async` pipe in tight loops (careful)
- 6) Missing cleanup for manual subscriptions
- 7) Running high-frequency events inside Zone
- 8) No virtual scrolling for huge lists
- Lazy Loading
- Module Lazy Loading
- Component Lazy Loading
- Bundle Optimization
- Tree Shaking
- Preloading Strategies
- RxJS Optimization
- Unsubscribe from Observables
- Async Pipe
- Share Operators
- Template Optimization
- Pure Pipes
- Avoid Function Calls in Templates
- AOT vs JIT
- Ahead-of-Time (AOT) Compilation
- Virtual Scrolling
- Service Workers and PWA
- Best Practices
- Measuring Performance
- Build Optimization
- Runtime Performance Tools
Angular Performance Optimization
Performance optimization techniques specific to Angular applications.
Change Detection
OnPush Change Detection Strategy
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-user',
template: `<div>{{ user.name }}</div>`,
changeDetection: ChangeDetectionStrategy.OnPush // Only check when inputs change
})
export class UserComponent {
@Input() user: User;
}
Detaching Change Detection
import { ChangeDetectorRef } from '@angular/core';
export class MyComponent {
constructor(private cdr: ChangeDetectorRef) {
// Detach from change detection
this.cdr.detach();
}
updateData() {
// Manual change detection
this.cdr.detectChanges();
}
}
TrackBy for ngFor
// ❌ Bad - re-renders all items
<div *ngFor="let item of items">{{ item.name }}</div>
// ✅ Good - only re-renders changed items
<div *ngFor="let item of items; trackBy: trackById">{{ item.name }}</div>
trackById(index: number, item: Item): number {
return item.id;
}
Signals vs Observables (performance + architecture)
High-signal interview framing:
- Signals are synchronous, dependency-tracked values. Angular can update only consumers that read a signal.
- Observables are async, push-based streams. Emissions often trigger change detection (via
asyncpipe or subscription code), which can be broader work.
1) Signals are synchronous & fine-grained; Observables are async & push-based
const count = signal(0);
const doubled = computed(() => count() * 2);
- Updating
countupdates only consumers ofcount/doubled. - Observables require
asyncpipe or subscriptions, and Angular doesn’t auto-track fine-grained dependencies the same way.
2) Signals can reduce change detection cost
- Observable emissions commonly participate in change detection.
- Signals enable more targeted updates for local state and derived UI values.
Senior phrasing:
In data-heavy UIs (dashboards, order books), signals scale better for local state because updates are more fine-grained.
3) Use Observables for I/O and async flows; Signals for local state
| Use case | Prefer |
|---|---|
| HTTP | Observable |
| WebSockets/market feeds | Observable |
| User events | Observable |
| Local component state | Signal |
| Derived UI state | Signal (computed) |
Bridge example (observable → signal):
import { toSignal } from '@angular/core/rxjs-interop';
const price = toSignal(price$);
4) Signals eliminate subscription management for local state
Observable pitfalls:
- manual unsubscribe patterns (
takeUntil,ngOnDestroy) - memory leaks from forgotten teardown
Signals:
- no subscription management for in-memory state
5) Computed signals are memoized (hidden performance win)
const filtered = computed(() =>
orders().filter((o) => o.price > threshold())
);
- Recomputes only when dependencies change.
- With RxJS you often need
map+distinctUntilChanged+shareReplayfor similar caching behavior.
Follow-up interview question: Should signals replace observables?
Answer:
No. They solve different problems. Observables handle async streams and cancellation; signals handle local synchronous state and derived values. They complement each other.
Performance pitfalls interviewers love (and fixes)
1) Default change detection everywhere
Problem: large trees get checked frequently on async events.
Fix:
- prefer
ChangeDetectionStrategy.OnPush - keep inputs immutable (new references)
- use signals for local state/derived values
2) Overusing Observables for local UI state
Problem: extra subscriptions + extra change detection + leak risk.
Fix:
- use signals for local UI toggles/derived values
3) *ngFor without trackBy
Problem: DOM churn and poor scrolling performance.
Fix: trackBy
4) Heavy logic in templates
Problem: template calls run frequently and hide cost.
Fix:
- precompute values (properties)
- use
computed()for derived state
5) async pipe in tight loops (careful)
Problem: multiplying work and re-render churn when used incorrectly.
Fix:
- subscribe/convert once (e.g.,
toSignal) - render from a stable array/value
6) Missing cleanup for manual subscriptions
Fix options:
``ts import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; this.stream$.pipe(takeUntilDestroyed()).subscribe(); ``
- prefer
asyncpipe takeUntilDestroyed()(Angular interop)
7) Running high-frequency events inside Zone
Fix:
ngZone.runOutsideAngular(...)for scroll/market-feed adapters- re-enter the zone only when you need UI updates
8) No virtual scrolling for huge lists
Fix:
cdk-virtual-scroll-viewport(render only what’s visible)
Lazy Loading
Module Lazy Loading
// app-routing.module.ts
const routes: Routes = [
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
},
{
path: 'user',
loadChildren: () => import('./user/user.module').then(m => m.UserModule)
}
];
Component Lazy Loading
import { Component, ViewChild, ViewContainerRef } from '@angular/core';
@Component({
selector: 'app-lazy-wrapper',
template: '<ng-container #container></ng-container>'
})
export class LazyWrapperComponent {
@ViewChild('container', { read: ViewContainerRef })
container: ViewContainerRef;
async loadComponent() {
const { HeavyComponent } = await import('./heavy.component');
this.container.createComponent(HeavyComponent);
}
}
Bundle Optimization
Tree Shaking
// ✅ Import only what you need
import { map, filter } from 'rxjs/operators';
// ❌ Don't import entire library
import * as _ from 'lodash';
Preloading Strategies
// app-routing.module.ts
import { PreloadAllModules } from '@angular/router';
@NgModule({
imports: [
RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules
})
]
})
export class AppRoutingModule {}
// Custom preloading strategy
export class CustomPreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
return route.data?.['preload'] ? load() : of(null);
}
}
RxJS Optimization
Unsubscribe from Observables
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
export class MyComponent implements OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
this.dataService.getData()
.pipe(takeUntil(this.destroy$))
.subscribe(data => {
// Handle data
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
Async Pipe
// ✅ Good - automatic subscription management
@Component({
template: `
<div *ngIf="data$ | async as data">
{{ data.name }}
</div>
`
})
export class MyComponent {
data$ = this.dataService.getData();
}
Share Operators
import { shareReplay } from 'rxjs/operators';
// Share single subscription among multiple subscribers
this.data$ = this.http.get('/api/data').pipe(
shareReplay(1)
);
Template Optimization
Pure Pipes
@Pipe({
name: 'expensiveCalc',
pure: true // Only recalculates when input changes
})
export class ExpensiveCalcPipe implements PipeTransform {
transform(value: number): number {
// Expensive calculation
return value * Math.random();
}
}
Avoid Function Calls in Templates
// ❌ Bad - called on every change detection
@Component({
template: `<div>{{ getFullName() }}</div>`
})
export class BadComponent {
getFullName() {
return this.firstName + ' ' + this.lastName;
}
}
// ✅ Good - computed once
@Component({
template: `<div>{{ fullName }}</div>`
})
export class GoodComponent {
get fullName() {
return this.firstName + ' ' + this.lastName;
}
}
AOT vs JIT
- AOT: Compiles templates at build time, smaller runtime cost, better errors.
- JIT: Compiles in the browser, faster builds during development.
- Recommendation: Use AOT for production, JIT for rapid local iteration.
Ahead-of-Time (AOT) Compilation
// angular.json
{
"projects": {
"app": {
"architect": {
"build": {
"configurations": {
"production": {
"aot": true,
"buildOptimizer": true,
"optimization": true
}
}
}
}
}
}
}
Virtual Scrolling
import { ScrollingModule } from '@angular/cdk/scrolling';
@Component({
template: `
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
<div *cdkVirtualFor="let item of items">
{{ item.name }}
</div>
</cdk-virtual-scroll-viewport>
`,
styles: [`
.viewport {
height: 400px;
}
`]
})
export class VirtualScrollComponent {
items = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`
}));
}
Service Workers and PWA
# Add service worker
ng add @angular/pwa
// App module
import { ServiceWorkerModule } from '@angular/service-worker';
@NgModule({
imports: [
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: environment.production
})
]
})
export class AppModule {}
Best Practices
- Use OnPush change detection when possible
- Lazy load feature modules
- Use trackBy with ngFor
- Unsubscribe from observables (use async pipe or takeUntil)
- Enable AOT compilation in production
- Use pure pipes for transformations
- Avoid function calls in templates
- Implement virtual scrolling for long lists
- Use Web Workers for heavy computations
- Enable service workers for caching
Measuring Performance
// Using Angular DevTools (browser extension)
// Or performance API
ngAfterViewInit() {
performance.mark('component-rendered');
performance.measure('init-to-render', 'component-init', 'component-rendered');
}
Build Optimization
# Production build
ng build --configuration production
# Analyze bundle size
ng build --stats-json
npx webpack-bundle-analyzer dist/stats.json
Runtime Performance Tools
- Angular DevTools (Chrome Extension)
- Source Map Explorer
- Webpack Bundle Analyzer
- Lighthouse
- Chrome DevTools Performance Panel