Migrating a Large Angular App to Signals + Zoneless
Zone.js's pitch was always "you never have to think about change detection." That was the right
default for 2016, but on a large app it means Angular re-checks far more of the component tree
than it needs to on every async event, and tracking down why a component re-rendered means
reading zone patches instead of your own code. Signals fix both problems: dependencies are
explicit, and provideZonelessChangeDetection() lets Angular stop guessing when something
changed because the signal graph tells it directly.
Don't do it as a big-bang rewrite
The migration that works is incremental:
- Turn on signals for new state first.
signal(),computed(), and the signal forms ofinput()/output()/model()are drop-in for new components. You don't need zoneless to start using signals — they work fine alongside zone.js. - Convert component state leaf-first. Start with presentational components that own simple local state. They're low-risk and give the team a feel for the signal APIs before touching anything with real business logic.
- Bridge RxJS at the edges, not everywhere. HTTP calls, WebSocket streams, and router events
stay in RxJS — that's what it's good at. Use
toSignal()to bring the result into the signal graph rather than converting every intermediate operator chain. - Flip
provideZonelessChangeDetection()last, once most of the tree is signal-driven. Until then, zone.js is still doing useful work for the parts you haven't converted.
What breaks when you go zoneless
The main failure mode is code that mutates state outside Angular's notification system and
expects a re-render anyway — a third-party library callback, a raw setTimeout, a mutated array
passed by reference. Under zone.js these "just worked" because zone.js patched the async APIs and
triggered a check regardless. Zoneless won't save you here: anything that isn't a signal write,
an async pipe emission, or inside markForCheck()/ApplicationRef.tick() needs an explicit
nudge. In practice this surfaces a handful of real bugs — state that was accidentally reactive
only because change detection ran so often it papered over the gap.
The payoff
Fewer, more targeted change detection passes; stack traces that point at your code instead of zone patches; and — the part that shows up in the bundle report — zone.js itself is gone, which is immediately visible in your initial JS payload.