D · Senior → Module 09
Module 09 · Questions 45–50

Performance

Change detection is the one they'll dig into. Everything else here follows from understanding it.

Q45

How does change detection work?

In one line

Zone.js notices that something happened — a click, an HTTP response, a timer — and Angular then checks every component from the root down.

click / HTTP response / setTimeout │ zone.js notices │ ▼ Angular walks the component tree from the root │ AppComponent checked ├ Header checked ├ OrderList checked │ ├ Row 1 checked │ ├ Row 2 checked ← all 200 rows, every time, │ └ … checked even if nothing changed └ Footer checked
What triggers itDOM events, HTTP responses, setTimeout / setInterval, promises
What it doesRe-evaluates every template binding in every component
When it hurtsLarge trees, long lists, or a function called from a template

This is fine for most apps. When it isn't, the answer is OnPush — not disabling zone.js.

Q46

OnPush change detection

@Component({
  selector: 'app-order-row',
  templateUrl: './order-row.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OrderRowComponent {
  @Input() order!: Order;
}

An OnPush component is re-checked only when:

  • an @Input reference changes
  • an event fires inside it
  • an async pipe in its template emits
  • you call markForCheck() yourself
OrderList is OnPush. The user adds an order. What appears on screen?
// version A
addOrder(o: Order) {
  this.orders.push(o);
}

// version B
addOrder(o: Order) {
  this.orders = [...this.orders, o];
}
A → nothing happens. The new order is in the array, but the screen doesn't change. The array REFERENCE is the same, so OnPush decides nothing changed and skips the check. B → the row appears. A new array means a new reference, so the input is seen as changed.

OnPush requires immutable data. Replace arrays and objects instead of mutating them. This is the single most common OnPush bug, and the reason people give up on it.

If you genuinely can't avoid mutation, inject ChangeDetectorRef and call markForCheck() — but treat that as a last resort.

Q47trackBy

Covered in full at Q9. The performance summary:

WithoutReassigning the array destroys and rebuilds every DOM node
WithAngular matches by id and touches only what changed
Biggest winLong lists that refresh often — a live dashboard, a polled grid
Also fixesLost focus, lost scroll position, lost input state

Track by a stable id. Tracking by index defeats the purpose the moment anything is inserted or reordered.

Q48Lazy loading

Covered at Q32. As a performance answer:

What it fixesTime to first paint — the browser downloads less before it can show anything
How you measure itThe chunk sizes printed by ng build
What to split firstAdmin and reporting areas — most users never open them
The refinementPreloadAllModules — small first load, then fetch the rest quietly
Q49

Avoiding unnecessary API calls

ProblemFix
A request on every keystrokedebounceTime(300)
The same term searched twicedistinctUntilChanged()
Old responses overwriting new onesswitchMap — cancels the previous
Two components asking for the same listshareReplay(1)
Reference data fetched on every screenCache it in a service
| async used twice on one observableshareReplay(1), or subscribe once
// typeahead: ~1 request per pause instead of one per letter
this.searchControl.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(t => this.api.search(t))
)

// reference data fetched once, shared by everyone
countries$ = this.http.get<Country[]>('/api/countries').pipe(
  shareReplay(1)
);
Say this

Mostly RxJS. On a search box, debounceTime and distinctUntilChanged cut it from one request per keystroke to one per pause, and switchMap cancels the superseded ones so a slow early response can't overwrite the right results. For reference data I cache with shareReplay so several components share one call rather than each firing their own.

Q50What else slows an Angular app down?
CauseFix
{{ getTotal() }} in a templateIt runs on every change detection cycle. Compute it once into a field.
Impure pipesSame problem — they run constantly. Keep pipes pure.
Rendering 5,000 rowsVirtual scroll (cdk-virtual-scroll-viewport) or paging
A huge initial bundleLazy loading; check the build output
Deep component trees, default CDOnPush on the leaves
Unnecessary subscriptionsasync pipe; unsubscribe properly

Angular 16+ signals are the newer answer: a signal tells Angular exactly what changed, so it can update just that binding rather than re-checking a whole tree. Worth knowing the name and the reason — you don't need to have used them.