Lesson 22 of 30
Zone.js used to re-check the whole app every time anything async happened, anywhere. Zoneless mode drops that net entirely — signals now tell Angular exactly what changed, and OnPush tells it exactly where to look.
16 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
For most of Angular's life, change detection ran on Zone.js — a library that monkey-patches every async API in the browser (setTimeout, Promise.then, DOM event listeners, fetch, all of it). Whenever any of those fired, anywhere in your app, Zone.js told Angular "something might have changed," and Angular walked the entire component tree checking for differences. It worked, but it was a blunt instrument: one button click could trigger a full-tree check even for components nowhere near the click.
Zoneless change detection — stable since v20.2, and the default for new apps in v21+ — removes Zone.js from the picture. Without its patching, nothing automatically tells Angular "check everything." Instead, the current default mental model is: components use OnPush change detection, and signals are what actually trigger a re-render. When a signal a template reads changes value, Angular knows precisely which component depends on it and schedules exactly that component for a check — not the whole tree.
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideZonelessChangeDetection } from '@angular/core';
import { AppComponent } from './app.component';
bootstrapApplication(AppComponent, {
providers: [provideZonelessChangeDetection()],
});
// counter.component.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<button (click)="count.set(count() + 1)">
Clicked {{ count() }} times
</button>
`,
})
export class CounterComponent {
count = signal(0);
}
No Zone.js is loaded at all — polyfills in angular.json doesn't even include zone.js. Clicking the button calls count.set(count() + 1). Because the template reads count() directly, Angular's reactive graph already knows this exact component depends on this exact signal — so setting it schedules a re-render of CounterComponent and nothing else. There was never a "digest the whole tree" step to begin with; the signal is the notification.
A click fires deep in the tree, inside the Chart component.
this.items.push(x) without a signal used to get away with it under Zone.js, because some unrelated async call elsewhere would eventually trigger a global check that happened to catch the mutation. Zoneless removes that accidental safety net — if nothing reactive changed, nothing re-renders.OnPush and signals; zoneless is really about removing the automatic-everywhere fallback, not about the component code you write day to day.The signals fundamentals lesson introduced signal() and computed() as reactive primitives; this lesson is where that reactivity stops being "nice for state management" and starts being the actual change-detection mechanism. It's a similar shift to how React re-runs a function component when its state changes — except Angular's signal graph knows the specific component to re-check, rather than re-running a function and diffing the result.