๐Ÿณ IntuitiveFE
Login
โ† All concepts

Angular Performance

โฑ๏ธ ~16-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

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/100

Core Web Vitals Impact

LCPINPCLSTTFBBundle
ChangeDetectionStrategy.OnPush

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.

ts
1import { Component, ChangeDetectionStrategy, Input, inject } from '@angular/core';
2import { AsyncPipe, NgFor } from '@angular/common';
3import { Store } from '@ngrx/store';
4import { 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})
22export 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)
38constructor(private cdr: ChangeDetectorRef) {}
39onExternalEvent() {
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
1/5 explored
๐ŸŽฏ

Challenge

Explore all 5 performance patterns. Toggle optimizations and watch your Core Web Vitals score climb.

Try it
๐ŸŽฏ

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.
1/4

What does provideExperimentalZonelessChangeDetection() require from every component in the app?