
From zone.js to Signals + SignalStore in Angular 20: A Multi‑Phase Migration That Won’t Break UX
A practical, CI‑guarded plan to move from zone.js change detection to Signals + SignalStore in Angular 20—measured, reversible, and safe for production UX.
“Make zoneless the last switch. First make your edges and state Signals‑aware, instrument everything, and keep a rollback lever within reach.”Back to all posts
I’ve migrated jittery real‑time dashboards, kiosk apps, and multi‑tenant portals to Angular 20+ Signals without torching UX. The pattern that works is boring on purpose: measure first, flip small switches, and keep a rollback lever. This guide is the playbook I’ve used at a telecom (ads analytics), a major airline (airport kiosk), and across my own products at AngularUX.
If you’re planning Q1 migrations or want an Angular expert to steady a legacy codebase, this shows exactly how I phase the move from zone.js to Signals + SignalStore—using Nx, Firebase, PrimeNG, Angular DevTools, and CI guardrails.
The Dashboard That Jittered—and the Day We Stopped Chasing Ticks
A real scene from production
At a telecom, our ad‑ops dashboard spiked to 5k events/minute. Zone.js thrashed: charts re‑rendered mid‑pan, PrimeNG tables lost scroll position, and support tickets lit up. We phased in Signals + SignalStore, added typed event schemas, and cut unnecessary change detection by 60% while improving p95 render by 27%—with zero user‑visible downtime.
Why a phased plan wins
Signals are fast, but migrations fail when teams leap straight to zoneless. Instead, make zoneless the last switch after edges and state are signal‑aware and observable.
Reversible at every step
No “big bang” outage windows
Telemetry‑driven trust with stakeholders
Why Angular 20 Teams Should Ditch zone.js Gradually
The technical rationale
Zone.js hid a lot of antipatterns that crept into codebases over the years. Signals make reactivity explicit and predictable. In Angular 20+, you get input signals, toSignal bridges for RxJS, and SignalStore for ergonomic domain state.
Signals eliminate excess dirty‑checking
Explicit effects beat implicit zone churn
Fine‑grained updates scale better for virtualized UIs
The delivery rationale
Treat this as a product change with feature flags, telemetry, and rollbacks. It keeps leadership calm and your roadmap intact.
Stakeholder‑friendly metrics (LCP, TTI, error rate)
Rollout by cohort (canary ➜ internal ➜ 5% ➜ 25% ➜ 100%)
No late‑night fire drills
Multi‑Phase Migration Plan: Signals, SignalStore, Zoneless
Phase 0 — Baseline and Risk Map
Before touching code, measure. In CI, add Lighthouse budgets and a threshold for error rates. In prod, log NgZone stability and route‑level timings to Firebase Performance/GA4.
Capture Core Web Vitals (LCP, INP), error rate, and render timings
Record change detection ticks on key routes with Angular DevTools
Tag high‑risk surfaces: PrimeNG tables, forms, WebSocket dashboards
Phase 1 — Introduce Signals at the Edges
Start where data enters the UI. Convert key RxJS streams to signals with toSignal, and introduce computed selectors near components. No user‑visible change yet, but ticks will drop.
Wrap network and stream sources with toSignal
Replace template async pipes gradually with signal reads
Keep OnPush everywhere; avoid creating new zone dependencies
Phase 2 — Adopt SignalStore for Domain State
Don’t rip out NgRx. Coexist. Use selectSignal to expose existing selectors to signal‑based components, and gradually move slices to SignalStore with typed events.
Create feature stores with explicit mutators and derived selectors
Bridge NgRx where needed via selectSignal
Document selectors/mutators for reviewers
Phase 3 — Gate Zoneless Behind a Feature Flag
Only when edges and stores are signals‑aware do we flip zoneless for canary users. Watch p95 hydration and INP. If anything regresses, turn the flag off—no redeploy needed.
Bootstrap with a runtime flag (Remote Config)
Rollout by audience cohort; watch hydration and error metrics
Keep instant rollback to zone.js
Phase 4 — SSR/Hydration, Accessibility, and UX Polish
Signals help you make accessibility updates deterministic. Ensure SR announcements and focus change via effects rather than zone‑triggered timing hacks.
Verify hydration timings and missing bindings
Focus/scroll management without zone side‑effects
ARIA/live‑region updates powered by effects
Phase 5 — Cleanup and Deprecations
Now make it boring: clean, measure, and move on. Document the new signal‑first patterns for new hires.
Remove zone.run wrappers and accidental global listeners
Delete brittle tests that relied on zone microtask timing
Lock in budgets and dashboards for ongoing monitoring
Code Walkthrough: SignalStore Bridges and Flagged Zoneless
Edge conversion: RxJS ➜ Signal
// user.data.service.ts
@Injectable({ providedIn: 'root' })
export class UserDataService {
private users$ = this.http.get<User[]>('/api/users').pipe(shareReplay(1));
users = toSignal(this.users$, { initialValue: [] });
// Derived signal keeps templates simple
activeCount = computed(() => this.users().filter(u => u.active).length);
constructor(private http: HttpClient) {}
}Coexist with NgRx using selectSignal
// adapters/ngrx-to-signals.ts
@Injectable({ providedIn: 'root' })
export class AccountsAdapter {
total = selectSignal(this.store, selectTotalAccounts);
selected = selectSignal(this.store, selectActiveAccount);
constructor(private store: Store) {}
}SignalStore slice with explicit mutators
// stores/devices.store.ts (@ngrx/signals)
import { signalStore, withState, withMethods, patchState } from '@ngrx/signals';
export type Device = { id: string; status: 'online'|'offline'; lastSeen: number };
export interface DevicesState { list: Device[]; filter: 'all'|'online'|'offline'; }
const initialState: DevicesState = { list: [], filter: 'all' };
export const DevicesStore = signalStore(
withState(initialState),
withMethods((store) => ({
setDevices(list: Device[]) { patchState(store, { list }); },
setFilter(filter: DevicesState['filter']) { patchState(store, { filter }); },
markOnline(id: string) {
patchState(store, ({ list }) => ({
list: list.map(d => d.id === id ? { ...d, status: 'online', lastSeen: Date.now() } : d)
}));
}
}))
);
@Injectable({ providedIn: 'root' })
export class DevicesFacade extends DevicesStore {
filtered = computed(() => {
const f = this.filter();
return f === 'all' ? this.list() : this.list().filter(d => d.status === f);
});
}Feature‑flagged zoneless bootstrap (Firebase Remote Config)
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { getRemoteFlag } from './app/flags';
// Angular 20+: provider name may vary; keep it behind a flag either way
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
(async () => {
const zoneless = await getRemoteFlag('zoneless_enabled');
const providers = [provideHttpClient()];
if (zoneless) providers.push(provideExperimentalZonelessChangeDetection());
await bootstrapApplication(AppComponent, { providers });
})();CI guardrails with Lighthouse and budgets
# .github/workflows/ci.yml (Nx + Lighthouse + budgets)
name: ci
on: [push, pull_request]
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npx nx run-many -t lint,test,build --parallel
- name: Lighthouse CI
run: |
npx http-server dist/apps/web -p 4200 &
npx lhci autorun --upload.target=temporary-public-storage
- name: Budget check
run: npx bundlesizeWhen to Hire an Angular Developer for Legacy Rescue
Red flags I see before migrations stall
If your app looks like this, bring in an Angular consultant early. You’ll ship faster and avoid death‑by‑refactor. I stabilize codebases via gitPlumbers (99.98% uptime on modernization work) and have done AngularJS ➜ Angular and JSP ➜ Angular lifts that stuck.
Unowned RxJS streams, memory leaks, zombie subscriptions
PrimeNG tables stutter with virtualization on
Forms rely on zone.run timing hacks
Outcomes you can expect
I’ve delivered this approach on a major airline kiosk (Docker hardware simulation, offline‑tolerant flows) and a telecom dashboard (typed telemetry, exponential retry).
Signal‑first state with measurable perf gains
A canary+flag rollout plan and a rollback switch
Documentation that lets new hires audit state paths
How an Angular Consultant Approaches Signals Migration
Engagement rhythm (typical)
Complex estates vary, but 4‑8 weeks handles most upgrades. For hotfix rescues, 2‑4 weeks. Discovery call within 48 hours; initial assessment in 5 business days.
Week 1: audit + metrics + risk map
Weeks 2‑3: Phase 1/2 implementation + CI guardrails
Week 4: canary zoneless + support + handoff
What I bring
Past work: employee tracking/payments at a global entertainment company, VPS schedulers for a broadcast network, insurance telematics dashboards, device portals for IoT fleets.
Angular 20, Signals/SignalStore, Nx, PrimeNG, Firebase
Real‑time dashboards, device integration, SSR, accessibility AA
Telemetries: Angular DevTools, GA4, Firebase Performance
Practical Takeaways and Next Steps
What to instrument next
Tie telemetry to releases so you can prove the win: Signals reduce ticks and stabilize UI under load. Keep the budgets in CI so regressions never merge.
Change detection ticks per route after each phase
Hydration p95 and INP deltas in CI and prod
Error taxonomy tied to state mutators/effects
Ready to move?
If you need an Angular expert to guide the rollout, I’m available as a remote Angular contractor. Let’s review your repo, agree on metrics, and ship the migration without risking UX.
Start with Phase 0 this week—no code changes required
Pilot Phase 1 on a single dashboard route
Book a code review to validate your plan
Questions to Ask Before Flipping Zoneless
Pre‑flight checklist
If any answer is “no,” finish that item before canary. The extra day now saves a week later.
Do we have a killswitch and a rollback to zone.js?
Which routes show the biggest tick reductions under Signals?
Are forms, virtual scroll, and WebSockets covered by tests?
Do we have p95 hydration/INP alerts and SLOs in place?
Are selectors/mutators documented so reviewers can reason about state?
Key takeaways
- Treat zoneless as a product change, not a refactor—baseline UX and error rates first.
- Introduce Signals at the edges, then move into domain state with SignalStore.
- Bridge RxJS/NgRx to Signals using toSignal/selectSignal and typed event schemas.
- Flip zoneless behind a remote feature flag; canary, observe, roll back instantly.
- Instrument hydration, change detection ticks, and render timings in CI and prod.
- Clear out zone‑dependent utilities only after zoneless is stable under real traffic.
Implementation checklist
- Baseline metrics: Core Web Vitals, Angular DevTools change detection ticks, error rates.
- Map risk: heavy PrimeNG tables, forms, and WebSocket dashboards first.
- Introduce toSignal at API edges; wrap UI‑critical selectors as computed.
- Create SignalStore slices with explicit mutators; keep NgRx coexisting via adapters.
- Gate zoneless bootstrap with a remote flag; test on internal canary first.
- Add Lighthouse/LCP budgets and Angular DevTools screenshot traces in CI.
- Ship a rollback plan: environment fallback to zone.js and feature flag killswitch.
- Audit and remove zone.run wrappers, async pipes depending on Zone, and flaky tests.
- Document selectors, mutators, and telemetry hooks for reviewers.
- Schedule incremental rollouts and office‑hours to support downstream teams.
Questions we hear from teams
- How long does a zone.js ➜ Signals + SignalStore migration take?
- Most teams ship in 4–8 weeks. I run Week 1 for audit/metrics, Weeks 2–3 for Signals at edges and SignalStore slices, and Week 4 for canary zoneless. Larger estates or heavy SSR may add 2–4 weeks.
- Do we need to replace NgRx to use SignalStore?
- No. Bridge NgRx with selectSignal and move slices over gradually. Many enterprise apps run NgRx for legacy modules and SignalStore for new features without issues.
- Will zoneless break third‑party libraries like PrimeNG?
- PrimeNG works well with OnPush and Signals. Test virtualization, overlays, and forms in canary. Keep a feature flag to fall back to zone.js instantly if a regression appears.
- What does an Angular consultant actually deliver here?
- A phased plan with metrics, code adapters, CI guardrails, and a flagged zoneless rollout. I pair with your team, refactor critical routes, and leave documentation and dashboards so the approach sticks.
- How much does it cost to hire an Angular developer for this migration?
- It depends on scope and team size. Typical engagements range 4–8 weeks. I offer a fixed‑fee assessment and milestone‑based delivery. Discovery call within 48 hours; assessment in 5 business days.
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