
From zone.js to Signals + SignalStore: A Multi‑Phase Migration Plan in Angular 20 Without Breaking UX
A battle‑tested rollout that keeps Core Web Vitals green, QA unblocked, and product owners calm—while you deprecate zone.js the right way.
Zoneless isn’t step one—it’s the reward for making state explicit. Signalize first, store second, then flip the switch when your metrics say go.Back to all posts
I’ve migrated more than a few enterprise dashboards off vibe‑coded state and into Signals + SignalStore. The trick isn’t code—it’s sequencing. If you jump straight to zoneless, you’ll get silent UI stalls and angry QA. Here’s the plan I use in Angular 20+ to keep UX smooth, measurable, and reversible.
This is the same playbook I used on advertising analytics for a telecom provider and an employee tracking system at a global entertainment company. We rolled out Signals first, hardened state with a SignalStore facade, and only then removed zone.js—route by route, behind flags, with telemetry watching every step.
The dashboard that stopped jittering—but QA still failed
If you need a senior Angular engineer to plan this migration, you can hire an Angular developer with Fortune 100 experience who’s done it under strict SLAs and zero‑downtime windows. I’ll show you the exact phases, guardrails, and code we use at AngularUX.
Scene from the trenches
On a telecom ads analytics dashboard, charts were over-rendering and list filters lagged. We signalized the hot paths and cut render counts by 42%. Great. But the day we trialed zoneless, two modals stopped updating a disabled button. No errors—just no change detection. The fix wasn’t to revert Signals; it was to finish the migration plan: schedule effects, replace async pipes, and introduce a SignalStore facade so templates only depended on reactive signals.
Why this matters as budgets reset for 2025
Q1 is hiring season for Angular talent; leaders want predictable upgrades.
Reducing change detection noise improves Core Web Vitals and SSR hydration.
Signals adoption sets you up for future Angular 21+ compiler gains.
Why zone.js removal breaks UX—and how to avoid it
Zoneless doesn’t introduce new problems—it reveals hidden coupling. Your plan must first remove that coupling by making state explicit and observable via Signals + SignalStore.
What zone.js was masking
Timers, DOM events, and third‑party callbacks implicitly triggered change detection.
Templates relying on async pipes or mutable inputs “just worked” because zone ran everywhere.
PrimeNG/material components often relied on run loops that zone kept in sync.
The Signals reality
Signals require explicit reads/writes; work outside Angular must notify via signals or mark for update.
Effects should be scheduled (e.g., requestAnimationFrame) for smoothness and micro‑task batching.
Interop with RxJS must be done via toSignal/toObservable.
A three‑phase plan: signalize, store, then zoneless
bootstrap config with coalescing and a store example:
Phase 1: Signals under zone.js
Code scaffolding:
Keep zone.js in place. Turn on event/run coalescing.
Convert high-churn inputs/derived state to signals and computed.
Introduce a SignalStore per domain slice; template reads only signals.
Phase 2: Harden state and remove implicit triggers
Bridge helpers:
Replace async pipe for hot streams with toSignal.
Move side effects to effect() with scheduling; avoid change detection thrash.
Add a feature flag (Firebase Remote Config) to pilot zoneless on routes.
Phase 3: Zoneless behind a flag, then app-wide
Bootstrap changes:
Stop importing zone.js; use the Angular 20 provider for zoneless change detection.
Patch third‑party components to emit through signals or manually trigger updates.
Roll out per route; watch telemetry; promote when <2% regression. Rollback is one flag.
Code walkthrough: SignalStore and zoneless bootstrapping
// ads-filters.store.ts
import { signalStore, withState, withComputed, withMethods } from '@ngrx/signals';
import { computed, effect, inject, signal } from '@angular/core';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { AdsApi } from './ads.api';
interface FiltersState {
query: string;
channel: 'tv' | 'mobile' | 'web' | 'all';
start: Date; end: Date;
loading: boolean;
results: readonly { id: string; spend: number }[];
}
const initialState: FiltersState = {
query: '', channel: 'all', start: new Date(), end: new Date(),
loading: false, results: []
};
export const AdsFiltersStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withComputed(({ query, channel, start, end }) => ({
params: computed(() => ({ q: query(), ch: channel(), start: start(), end: end() }))
})),
withMethods((store, api = inject(AdsApi)) => ({
setQuery(q: string) { store.query.set(q); },
setChannel(ch: FiltersState['channel']) { store.channel.set(ch); },
setRange(start: Date, end: Date) { store.start.set(start); store.end.set(end); },
search: () => {
store.loading.set(true);
const results$ = api.search(toObservable(store.params)); // legacy RxJS service
const resultsSig = toSignal(results$, { initialValue: [] as FiltersState['results'] });
effect(() => {
// Schedule to next frame for smoother UI during rapid input
const r = resultsSig();
requestAnimationFrame(() => { store.results.set(r); store.loading.set(false); });
});
}
}))
);<!-- ads-filters.component.html -->
<input type="search" [value]="store.query()" (input)="store.setQuery($any($event.target).value)" />
<p-dropdown [options]="channels" [ngModel]="store.channel()" (onChange)="store.setChannel($event.value)"></p-dropdown>
<button pButton [disabled]="store.loading()" (click)="store.search()">Search</button>
<p-table [value]="store.results()"></p-table>// main.ts (Phase 1 - with zone.js + coalescing)
import { bootstrapApplication, provideZoneChangeDetection } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true })
]
});// main.zoneless.ts (Phase 3 - pilot zoneless)
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideZonelessChangeDetection } from '@angular/core'; // Angular 20 zoneless provider
bootstrapApplication(AppComponent, {
providers: [provideZonelessChangeDetection()]
});Notes:
- Use the Angular 20 zoneless provider available in your version; confirm the exact symbol in release notes.
- Keep a feature flag to choose between main.ts and main.zoneless.ts builds in CI.
SignalStore for a filter panel (ads analytics example)
Interop: toSignal/toObservable for legacy services
Bootstrap: coalescing today, zoneless tomorrow
Progressive rollout: flags, metrics, and rollback
# .github/workflows/deploy.yml
name: deploy
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npm run build:with-zone # ng build -c production
- run: npm run build:zoneless # ng build -c zoneless
- name: Upload
run: |
firebase deploy --only hosting:with-zone --project $PROJECT --token $FIREBASE_TOKEN
firebase deploy --only hosting:zoneless --project $PROJECT --token $FIREBASE_TOKENIn Firebase Hosting, route traffic via headers or Remote Config: if zoneless flag is true for the user/route, rewrite to the zoneless artifact. If a regression appears, disable the flag—instant rollback.
Feature flags at the route level
Firebase Remote Config or environment toggles
CI builds two artifacts: with-zone and zoneless
Telemetry you must watch
Angular DevTools: change detection cycles per interaction
Firebase Performance: custom traces around critical flows
Lighthouse/CrUX: CLS, INP, LCP deltas <2% regression
Rollback in one line
Keep npm alias for with-zone build in deploy script
Prefer canary rollout to 5% of traffic before 100%
Real‑world notes: PrimeNG, WebSockets, and forms
// websocket -> signal for real-time dashboards
const updates$ = socket$.pipe(
map((e: TypedEvent) => normalize(e)),
retryBackoff({ initialInterval: 500, maxInterval: 8000, resetOnSuccess: true })
);
export const updatesSig = toSignal(updates$, { initialValue: EMPTY_SNAPSHOT });
// effect that renders charts in the next frame
effect(() => {
const snapshot = updatesSig();
requestAnimationFrame(() => charts.update(snapshot));
});In production telematics dashboards, this pattern eliminated jitter and protected INP while still giving real‑time feel.
PrimeNG and third‑party components
Upgrade to latest PrimeNG; prefer signal-friendly bindings where available.
If a component updates outside Angular, wrap emissions into signals or call set() on a store.
WebSockets and telemetry streams
Use typed event schemas; convert to signals via toSignal for render stability.
Backoff with exponential retry; isolate reconnection effects to avoid template thrash.
Forms migration
Keep Reactive Forms; mirror form.valueChanges with toSignal for computed UI.
For form-driven UIs, schedule effects to rAF to prevent input jank.
How an Angular consultant approaches Signals migration
If you need a remote Angular developer or Angular contractor to lead this, I can run the playbook, train your team, and leave you with CI guardrails. See how we rescue chaotic code at gitPlumbers (99.98% uptime during modernizations) and how IntegrityLens scaled to 12k+ interviews with stable Signals-driven flows.
Assessment (week 1)
Angular DevTools audit: render counts, CD cycles, hotspots
Inventory async sources: timers, sockets, 3rd‑party callbacks
Risk map by routes/features; choose a pilot
Implementation (weeks 2–4)
Signalize high‑churn components; introduce SignalStores
Replace async pipes on hot paths with toSignal
Schedule effects; enable coalescing; write regression tests
Rollout (weeks 4–6)
Pilot zoneless behind flags; verify metrics
Expand route by route; train team on patterns
Finalize docs and rollback playbook
Measurable outcomes and what to instrument next
Once you’re fully zoneless, your team owns change detection explicitly. That clarity compounds: fewer flaky tests, predictable performance, simpler mental model. And yes—product owners feel the difference.
Targets I hold teams to
Render count reduction ≥30% on hot routes
No more than 2% regression on LCP/INP/CLS per rollout
Crash‑free sessions ≥99.9% during migration
Next steps to harden the system
SSR + hydration metrics with GA4/Firebase Analytics
OpenTelemetry traces for store actions/effects
Feature-flagged experiments on scheduling strategies
Questions teams ask before hiring
If you want me to review your codebase and plan the rollout, reach out—discovery call within 48 hours, assessment in 1 week.
Do we need NgRx if we move to SignalStore?
For real‑time dashboards, I often keep NgRx for effects and action logging but expose a SignalStore facade to components. It gives you typed streams, optimistic updates, and stable template reads. We can trim boilerplate where Signals remove complexity.
Will zoneless break PrimeNG or legacy libraries?
Most modern libraries work fine if events are wrapped into Signals or you trigger store.set() in callbacks. In worst cases, we shim with toSignal/toObservable or isolate the component behind a facade.
How do we prove ROI to stakeholders?
We baseline render counts and Core Web Vitals, then show deltas per rollout. In one project, a Signals + tokens refresh lifted Lighthouse from 78 → 96 and cut renders by 42%. Stakeholders like green charts and reversible flags.
Key takeaways
- Treat zoneless as the final step, not the first. First adopt Signals and SignalStore behind feature flags.
- Measure before/after with Angular DevTools, Firebase Performance, and Lighthouse to prove no UX regressions.
- Migrate feature-by-feature using a SignalStore facade and interop helpers (toSignal/toObservable) to de-risk.
- Coalesce events and schedule effects to eliminate jitter before removing zone.js.
- Use CI guardrails (test matrix + toggles) so rollbacks are one-line and zero-downtime.
Implementation checklist
- Inventory components by risk: async-heavy, PrimeNG-heavy, forms, websockets, kiosk/offline.
- Introduce Signals while keeping zone.js; convert top-level inputs and derived state to computed signals.
- Stand up a SignalStore per domain slice; bridge existing RxJS/NgRx via toSignal/toObservable.
- Enable event coalescing and schedule effects with requestAnimationFrame before zoneless.
- Pilot zoneless on a low-risk route behind Remote Config/feature flags.
- Instrument Core Web Vitals and change detection counts; block rollout if regressions >2%.
- Remove zone.js only after “islands” pass QA; swap in the zoneless provider app-wide.
- Keep a rollback plan: env flag + npm alias to re-enable zone.js and prior build.
Questions we hear from teams
- How much does it cost to hire an Angular developer for a Signals migration?
- Typical engagements start with a 1-week assessment, then 3–6 weeks of implementation and rollout. Fixed-fee assessments are available; delivery is time-and-materials with clear milestones and a rollback plan.
- How long does an Angular upgrade and zoneless migration take?
- Signals + SignalStore adoption can begin in week 2. Zoneless pilots usually ship by weeks 4–6, with route-by-route rollout over 1–3 additional weeks depending on app size and risk.
- Will our team need to rewrite NgRx if we adopt SignalStore?
- No. Keep NgRx for effects and analytics while exposing a SignalStore facade to components. Bridge with toSignal/toObservable. You can retire NgRx slices gradually if Signals cover your needs.
- What CI guardrails do you recommend?
- Build both with-zone and zoneless artifacts, run Lighthouse/Angular DevTools benchmarks in CI, and toggle rollout with Remote Config. Keep an instant rollback by disabling the flag.
- What’s included in a typical Angular consultant engagement?
- Discovery, audit, migration plan, SignalStore scaffolding, coalescing/scheduling strategy, CI/test updates, pilot rollout, training, and handoff docs. Discovery call within 48 hours; assessment delivered within 1 week.
Ready to level up your Angular experience?
Let AngularUX review your Signals roadmap, design system, or SSR deployment plan.
NG Wave
Angular Component Library
A comprehensive collection of 110+ animated, interactive, and customizable Angular components. Converted from React Bits with full feature parity, built with Angular Signals, GSAP animations, and Three.js for stunning visual effects.
Explore Components