Angular Performance
โฑ๏ธ ~16-minute bite ยท solve the sandbox to master
5-Year-Old Metaphor
โ The physical, real-world picture. No jargon.Angular performance = tuning a sports car. Every optimization targets a different part of the powertrain.
- OnPush = lightweight chassis โ less metal to move means faster acceleration. Fewer CD cycles = faster response to user interaction.
- @defer = fuel injectorโ only burn fuel when the engine actually needs it. Don't load the chart component until the user scrolls to it.
- Virtual scroll = aero kit โ reduces drag when pushing through long lists. Constant DOM size = constant performance.
- Lazy loading = pit stop strategyโ don't carry spare tyres you don't need yet. Download route chunks on demand.
- Preloading = tyre warmersโ heat the tyres in the background so they're ready the instant you need them.
Interactive Sandbox
โ Move something, see it react instantly.Performance Score
42/100Core Web Vitals Impact
OnPush tells Angular: only run change detection on this component when its @Input() references change, an event originates within it, or an async pipe emits. Dramatically reduces CD cycles.
| 1 | import { Component, ChangeDetectionStrategy, Input, inject } from '@angular/core'; |
| 2 | import { AsyncPipe, NgFor } from '@angular/common'; |
| 3 | import { Store } from '@ngrx/store'; |
| 4 | import { Observable } from 'rxjs'; |
| 5 | ย |
| 6 | // โ OnPush โ Angular skips this component unless: |
| 7 | // 1. An @Input() reference changes (not deep equality โ new object/array reference) |
| 8 | // 2. An event fires from within this component |
| 9 | // 3. An Observable bound with async pipe emits a new value |
| 10 | // 4. ChangeDetectorRef.markForCheck() is called manually |
| 11 | @Component({ |
| 12 | selector: 'app-task-list', |
| 13 | standalone: true, |
| 14 | imports: [AsyncPipe, NgFor], |
| 15 | changeDetection: ChangeDetectionStrategy.OnPush, // โ the key line |
| 16 | template: ` |
| 17 | <div *ngFor="let task of tasks$ | async; trackBy: trackById"> |
| 18 | {{ task.title }} |
| 19 | </div> |
| 20 | `, |
| 21 | }) |
| 22 | export class TaskListComponent { |
| 23 | @Input() filter!: string; // OnPush triggers when this ref changes |
| 24 | tasks$: Observable<Task[]> = inject(Store).select(selectFilteredTasks); |
| 25 | ย |
| 26 | trackById = (_: number, task: Task) => task.id; // stable key for *ngFor |
| 27 | } |
| 28 | ย |
| 29 | // โ Mutation breaks OnPush โ NEVER do this with OnPush components: |
| 30 | // this.tasks.push(newTask); // same reference โ OnPush won't re-check |
| 31 | // this.task.title = 'new title'; // same reference โ change not detected |
| 32 | ย |
| 33 | // โ Always return new references with OnPush: |
| 34 | // this.tasks = [...this.tasks, newTask]; // new array โ triggers check |
| 35 | // this.task = { ...this.task, title: 'new title' }; // new object |
| 36 | ย |
| 37 | // Manual check when needed (e.g., after setTimeout, non-Angular events) |
| 38 | constructor(private cdr: ChangeDetectorRef) {} |
| 39 | onExternalEvent() { |
| 40 | // do work... |
| 41 | this.cdr.markForCheck(); // queue this component for next CD cycle |
| 42 | } |
- โOnPush works with async pipe โ the pipe calls markForCheck() automatically when the Observable emits
- โSignals (Angular 16+) are natively OnPush-compatible โ components using signals() auto-track dependencies
- โNever combine OnPush with mutable state โ the component won't re-render after mutation
Challenge
Explore all 5 performance patterns. Toggle optimizations and watch your Core Web Vitals score climb.
Why Should I Care?
โ The exact interview question + the bug it kills.Q: What's the difference between lazy loading and @defer?
Lazy loading is route-level โ the chunk downloads when the user navigates to a new route. @defer is component-level within an already-loaded route โ the deferred component's chunk downloads based on a trigger (viewport, interaction, timer). Use lazy loading for feature areas; use @defer for heavy below-fold components within a page.
Q: How does trackBy improve *ngFor performance?
Without trackBy, Angular uses object identity (===) to diff the list. Every change triggers full DOM re-creation. trackBy provides a stable key (id field) โ Angular only creates/destroys DOM nodes for truly added or removed items, preserving and reusing existing nodes.
Q: What Core Web Vitals does Angular performance most affect?
LCP (Largest Contentful Paint): initial bundle size, lazy loading, @defer, SSR. INP (Interaction to Next Paint): change detection frequency โ OnPush, signals, and zoneless Angular are the main levers. CLS (Cumulative Layout Shift): NgOptimizedImage sets explicit dimensions to reserve space before images load.
The Deep Dive
โ Spec refs, engine internals, the minutiae.Advanced performance tools, APIs, and the zoneless future:
- 01
@defer trigger types in full: on idle (requestIdleCallback), on viewport (IntersectionObserver), on interaction (click/keydown on placeholder), on timer(ms), on immediate (next tick), when (expression). Combine: @defer (on viewport; prefetch on idle).
- 02
NgOptimizedImage (NgModule: NgOptimizedImage or standalone import): automatic srcset generation, lazy loading by default, priority attribute for LCP images, built-in blur-up placeholders. Replace <img src> with <img ngSrc>.
- 03
Angular DevTools Profiler: records a CD cycle flame chart. See which components ran CD, how long each took, and which triggered re-renders in their children. Available as a browser extension.
- 04
Zoneless Angular: provideExperimentalZonelessChangeDetection() removes Zone.js (~13KB). CD runs only on signal reads and markForCheck() calls. Requires all state to flow through signals or explicit CD triggers โ no setTimeout/Promise magic.
- 05
Bundle analysis workflow: ng build --configuration production --stats-json, then npx webpack-bundle-analyzer dist/[project]/stats.json. Look for: third-party libraries in the main bundle, duplicate polyfills, accidentally eager feature modules.
Interview Questions
โ Real questions from real interviews โ with answers.Lazy loading = route-level chunk (downloads on navigation); @defer = component-level chunk within an already-loaded route.
trackBy provides a stable key so Angular reuses DOM nodes for unchanged items instead of recreating them.
on viewport = below-fold content; on interaction = editor/modal; on idle = proactive prefetch; when = custom condition.
It renders only the visible rows by recycling a fixed window of DOM nodes; all items must have the same fixed height.
Bundle/lazy โ LCP; OnPush/signals โ INP; NgOptimizedImage โ CLS; preloading โ subsequent LCP/TTFB.
provideExperimentalZonelessChangeDetection() removes zone.js โ but all CD must be explicitly driven by signals or markForCheck().
Memory Game
โ Quick quiz โ lock the concept in long-term memory.What does provideExperimentalZonelessChangeDetection() require from every component in the app?