1. Zoneless Change Detection & OnPush
Back in Week 3, you learned that Zone.js patches async browser APIs so Angular knows
"something happened, time to check for changes" — then re-checks the entire
component tree by default. OnPush narrows that: a component only re-checks
when one of its @Input()s changes by reference, an event originates inside
it, or an async-piped Observable emits.
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-product-card',
templateUrl: './product-card.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductCard {}
Signals make OnPush essentially free to adopt everywhere: a signal read in
a template automatically and precisely marks that component for a check when the signal
changes — you get OnPush's performance benefit without OnPush's
classic downside (accidentally mutating an object in place and wondering why the view
didn't update).
import { provideZonelessChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideZonelessChangeDetection(), // no Zone.js at all
],
};
Zoneless removes Zone.js from the bundle and its runtime patching overhead entirely —
Angular now relies purely on explicit signals of "something might have changed" (a
signal write, a manually called markForCheck()) instead of monkey-patching
every async API in the browser to guess. An app built signal-first, with
OnPush everywhere, is already most of the way there — zoneless just removes
the safety net you no longer need.
2. Profiling with Angular DevTools & the Chrome Performance Panel
You installed Angular DevTools back in Week 2 — its Profiler tab records a timeline of every change-detection cycle, showing exactly which components were checked and how long each took. This is where you find "this component re-renders far more than it should."
// In Angular DevTools -> Profiler tab:
// 1. Click "Record", interact with your app, click "Stop"
// 2. Each bar in the timeline is one change-detection cycle
// 3. Click a bar to see every component checked during it, and how long each took
// 4. A component checked on EVERY cycle, with no visible reason, is your first suspect
Chrome's own Performance panel operates one level lower — a full flame chart of everything the main thread did, including script execution, layout, and paint. Reach for it when the problem isn't "which component re-rendered" but "why did the browser freeze for 200ms" — a long, uninterrupted JavaScript task is exactly what a flame chart makes visually obvious.
3. Bundle Analysis & Lazy-Loading Strategy Review
You set up lazy loading back in Week 11 — bundle analysis is how you verify it's actually working, and catch a dependency that snuck into the initial bundle by accident.
ng build --stats-json
npx esbuild-visualizer --metadata dist/my-app/stats.json --filename bundle-report.html
The resulting treemap shows exactly what's in each chunk, sized proportionally to actual bytes — a huge, unexpected block in your initial bundle (rather than a lazy chunk) is the classic sign of an accidentally-eager import, like a heavy charting library imported at the top of a shared file instead of only inside the lazy-loaded route that actually uses it.
This is the moment to confirm the code-splitting plan from Week 11 actually produced the chunk sizes you expected — bundle analysis is the tool that turns "I lazy-loaded this route" from an assumption into a measured fact.
4. Image Optimization with NgOptimizedImage
Images are frequently the single biggest contributor to a slow page load.
NgOptimizedImage is a directive enforcing best practices you'd otherwise
have to remember by hand every time.
<img
[ngSrc]="product.imageUrl"
width="400"
height="300"
[priority]="isAboveTheFold"
alt="{{ product.name }}"
/>
ngSrcinstead ofsrc— enables automaticsrcsetgeneration and lazy loading by default.width/heightare required — without them, the browser can't reserve space before the image loads, causing layout shift as content jumps once it arrives.prioritydisables lazy loading for exactly one image — the largest visible one on initial load (the Largest Contentful Paint candidate), which should load immediately rather than being deferred.
Skipping priority on your actual LCP image is a common mistake — lazy
loading is right for most images, but the one thing users see first shouldn't be
deferred at all.
5. Virtual Scrolling with the CDK
Rendering a list of thousands of items — even simple ones — means thousands of live DOM nodes, which is slow to create and slow for the browser to manage. Virtual scrolling renders only the handful of rows currently visible in the viewport, recycling DOM nodes as the user scrolls.
<cdk-virtual-scroll-viewport itemSize="56" class="viewport">
<div *cdkVirtualFor="let item of items()" class="row">
{{ item.name }}
</div>
</cdk-virtual-scroll-viewport>
.viewport {
height: 500px; /* the viewport needs an explicit, bounded height to know how much to render */
}
itemSize tells the viewport the fixed height of each row up front, so it
can calculate scroll position and total scrollable height without ever needing to
measure every single item. For a list of 10,000 rows, this is the difference between
10,000 live DOM nodes and roughly a dozen.
6. Memory Leak Detection & Fixes
You already know the most common source of a memory leak — an unmanaged subscription from Week 15. Chrome's Memory panel is how you actually confirm one exists rather than suspecting it.
// In Chrome DevTools -> Memory panel:
// 1. Take a heap snapshot (baseline)
// 2. Navigate to a component, then navigate AWAY from it (it should be destroyed)
// 3. Repeat step 2 several times
// 4. Take a second heap snapshot, filter by the component's class name
// 5. If instances of a supposedly-destroyed component are still present -> it's leaking
A component instance surviving navigation almost always means something is still holding
a reference to it — most commonly an unmanaged subscription (fixed with
takeUntilDestroyed(), Week 15) or an event listener registered on a
long-lived global object (like document or window) that was
never removed in ngOnDestroy.
export class ResizeAwarePanel implements OnDestroy {
private handleResize = () => this.recalculate();
constructor() {
window.addEventListener('resize', this.handleResize); // window outlives every component
}
ngOnDestroy() {
window.removeEventListener('resize', this.handleResize); // without this line: a leak
}
}
7. Hands-on Exercise
Profile the dashboard app and fix three real bottlenecks
Apply this lesson's tools to your own Weeks 9-19 dashboard project — measured, not guessed.
Requirements:
- Record an Angular DevTools profile while interacting normally with your dashboard. Identify one component that re-checks more often than it should, and fix it (
OnPush, converting a mutated object to an immutable update, or both). Record a second profile and note the measured difference. - Run a bundle analysis. Identify one thing in your initial bundle that shouldn't be there, and move it behind lazy loading or
@defer. Compare initial-bundle size before and after. - Find one place your app renders a long list without virtual scrolling, and convert it to
cdk-virtual-scroll-viewport. Confirm — via the Elements panel's live DOM node count — that far fewer nodes exist after scrolling through the full list. - Write up your three fixes as short before/after notes: what the profiler or bundle report showed, what you changed, and the measured result.
Resist optimizing anything the profiler didn't actually flag — the whole point of this exercise is measuring before you act. An unmeasured "optimization" can just as easily make things worse or fix a problem that was never actually significant.
8. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why do signals make OnPush safer to adopt than it was with plain mutable objects and Zone.js?
Why do signals make OnPush safer to adopt than it was with plain mutable objects and Zone.js?
The classic OnPush pitfall was mutating an object in place — since the reference never changed, OnPush never noticed and the view silently failed to update. A signal read in a template registers as a precise dependency automatically; when that signal changes, Angular marks exactly that component for a check, with no reliance on reference equality of a plain object at all.
Q2
What does removing Zone.js via provideZonelessChangeDetection() actually remove?
What does removing Zone.js via provideZonelessChangeDetection() actually remove?
The runtime overhead of monkey-patching every async browser API (setTimeout, event listeners, Promises) just so Angular can guess "something might have happened, time to check." Without Zone.js, Angular instead reacts to explicit signals — signal writes and manually triggered checks — which is both a smaller bundle (no Zone.js shipped at all) and more precise, at the cost of needing your reactivity to genuinely be signal- or explicitly-triggered throughout.
Q3
Why are width and height required attributes on NgOptimizedImage, not just recommended?
Why are width and height required attributes on NgOptimizedImage, not just recommended?
Without a known width and height, the browser has no way to reserve space for the image before it finishes loading — surrounding content renders first, then jumps as the image arrives and pushes everything down. This layout shift is a real, measured harm to user experience (and a tracked web performance metric), so NgOptimizedImage enforces the dimensions rather than leaving it as an easy-to-forget best practice.
Q4
You take two heap snapshots — before and after repeatedly navigating to and away from a component — and instances of that component's class are still present. What does that indicate?
You take two heap snapshots — before and after repeatedly navigating to and away from a component — and instances of that component's class are still present. What does that indicate?
Something is still holding a reference to the "destroyed" component instance, preventing garbage collection — most commonly an unmanaged RxJS subscription, or an event listener registered on a long-lived object like window or document that was never removed in ngOnDestroy. The component should have no reason to exist in memory after navigating away, so its continued presence is a direct sign of a leak.