What does it mean that an Angular v21+ application is zoneless by default, and what does dropping zone.js gain you?
answer
- who tells Angular to check
- no more monkey-patched browser APIs
- notifications from Angular's own APIs
- polyfills entry in angular.json
basics
~20 sSince v21, Angular schedules change detection from its own notifications (template-read signal writes, markForCheck, bound listeners, setInput) instead of zone.js patching browser APIs. Dropping zone.js means fewer needless checks, a smaller bundle, faster startup and readable stack traces.
solid answer
~50 sUntil v21 an Angular app loaded `zone.js`, which monkey-patches timers, promises, DOM event listeners and XHR so Angular learns when *any* async task ends and then runs a check of the view tree. Since v21 the bootstrap installs a zoneless scheduler unless you add `provideZoneChangeDetection()`: a check is scheduled only when an Angular API notifies it. The documented notifications are a signal read in a template being set, `ChangeDetectorRef.markForCheck()` (which `AsyncPipe` calls), a bound template or host listener firing, `ComponentRef.setInput()`, and attaching a view that one of those marked dirty. The gains: checks run when state changed rather than whenever some task finished, the zone.js payload and startup cost disappear, stack traces lose the zone frames, and nothing depends on patching APIs such as native `async`/`await`. To actually drop it, remove `zone.js` and `zone.js/testing` from the `polyfills` of the `build` and `test` targets in `angular.json`, then uninstall the package.
code
ts · 5 linesimport {bootstrapApplication} from '@angular/platform-browser';
import {App} from './app/app';
// Angular v21+: no change-detection provider needed, the app is zoneless.
bootstrapApplication(App, {providers: []});go deeper
Recall that since v21 Angular runs without zone.js by default, and that templates update when signals change or events fire. Know that zone.js lives in the polyfills.
List the notifications that schedule a check and explain why a plain field changed in a timer does not render. Know the four documented benefits and the removal steps.
Show you can audit an existing codebase for hidden zone reliance before dropping zone.js: timers and foreign callbacks mutating fields, NgZone observables, and tests that assume automatic checks.
Frame zoneless as a trade: automatic but blind scheduling versus explicit notifications. Discuss the cost of enforcing that discipline across teams and third-party components.
## What zone.js used to do **Change detection** is Angular's pass over the component view tree that compares template bindings with their previous values and updates the DOM where they differ. Something has to decide *when* that pass runs. For most of Angular's history the answer was **zone.js**: a library that monkey-patches browser APIs (`setTimeout`, `Promise`, `addEventListener`, `XMLHttpRequest` and many more) so that Angular's `NgZone` hears about every asynchronous task. When the zone had no more pending microtasks, Angular ran `ApplicationRef.tick()` and checked the tree. That model is automatic, but blind: zone.js knows a task finished, not whether it changed anything the UI shows. A scroll listener in a charting library triggers checks just as often as a click that updates a form. ## What changed in v21 - Zoneless change detection became **stable in v20.2** as `provideZonelessChangeDetection()` (it was experimental earlier, under a different provider name). - In **v21 it became the default**. `bootstrapApplication()` and `bootstrapModule()` now install the zoneless scheduler and a no-op `NgZone` unless the application adds `provideZoneChangeDetection()`, which opts back into zone-based scheduling. - New code therefore needs nothing to be zoneless; the documentation says to check that `provideZoneChangeDetection` is not used anywhere to override the default. ## What tells Angular to check now Without a zone, Angular waits for **notifications** from its own APIs: | Notification | Typical source | |---|---| | A signal read in a template is set | `count.set(1)` in any callback | | `ChangeDetectorRef.markForCheck()` | called for you by `AsyncPipe` | | A bound template or host listener runs | `(click)`, `host: {'(keydown)': ...}` | | `ComponentRef.setInput()` | dynamic components | | A view marked dirty is attached | views created with the APIs above | Several notifications in the same turn **coalesce** into one scheduled check. Code that changes state some other way (assigning a plain class field inside a timer or a third-party callback) gives Angular no signal at all, so the screen does not change. ## Removing zone.js from the build Making the scheduler zoneless does not by itself remove the library from an existing project. To drop it: 1. Remove `zone.js` from the `polyfills` array of the `build` target in `angular.json`. 2. Remove `zone.js/testing` (and `zone.js`) from the `polyfills` of the `test` target. 3. If the project uses an explicit `polyfills.ts`, delete its `import 'zone.js';` and `import 'zone.js/testing';` lines. 4. Run `npm uninstall zone.js`. ```json "build": { "options": { "polyfills": [] } } ``` In development mode, an app that calls `provideZonelessChangeDetection()` while zone.js is still loaded logs warning **NG0914** suggesting you remove it, because the patches still cost payload and startup even though nothing uses them. ## What you gain The Angular documentation lists four benefits: - **Performance**: checks run after a real state notification rather than after every async task. - **Core Web Vitals**: zone.js adds payload and startup time; removing it removes both. - **Debugging**: stack traces no longer pass through zone frames, and there is no "ran outside the Angular zone" class of bug. - **Ecosystem compatibility**: zone.js cannot patch every API; native `async`/`await` had to be downleveled for it to work, and some libraries clash with its patches. ## A common misreading "Zoneless" is sometimes heard as "Angular stopped doing change detection" or "Angular now polls". Neither is true. Angular still runs the same check over the view tree; it simply stops treating *every* finished async task as a reason to run it. The scheduler sits idle until one of the notifications above arrives, then runs one check, timed by a race between `setTimeout` and `requestAnimationFrame` so the update normally lands before the next paint. If nothing notifies it, nothing runs, which is exactly why a value assigned to a plain field from a timer never appears on screen. The fix is never "turn zone.js back on for that component"; it is to make the state change visible to Angular. ## What does not change The check itself is the same top-down pass over the view tree, and `OnPush` and dirty marking work as before. Zoneless changes *when* a pass is triggered, not how a view is refreshed. The practical consequence is a discipline: state the template shows should live in **signals** (or reach the template through `AsyncPipe`), and code that mutates plain fields from timers or foreign callbacks has to be rewritten, because nothing is watching those tasks any more.
- If an Angular v21 app is zoneless but zone.js is still listed in polyfills, what happens?The scheduler stays zoneless, so zone.js triggers no checks, but its patches still load and run, costing payload and startup time. If the app explicitly calls `provideZonelessChangeDetection()`, development mode also logs NG0914 recommending that you remove zone.js from the `polyfills` in `angular.json`.
- Does going zoneless require every Angular component to use OnPush?No. The documentation calls OnPush a recommended step, not a requirement. An Eager component still works if the state it shows is changed through something that notifies Angular: signals, `markForCheck()`, `AsyncPipe` or bound listeners. What breaks is relying on zone.js to notice a timer or callback that mutated a plain field.
saying these in an interview costs you the question
- Zoneless means Angular no longer runs change detection at all.
- Zoneless apps re-check the whole tree on every animation frame.
- Setting the scheduler zoneless automatically removes zone.js from the bundle.
- Every component must be OnPush before an app can run zoneless.
- provideZoneChangeDetection() is how you enable zoneless in v21.