
Don’t Flip the Switch: A Safe, Multi‑Phase Plan to Migrate From zone.js to Signals + SignalStore in Angular 20+
A practical, metrics‑driven migration path to Signals and SignalStore that avoids UX regressions—even with PrimeNG, websockets, and legacy RxJS code.
Zoneless isn’t a toggle—it’s the last step of a disciplined Signals rollout with metrics, boundaries, and a rollback plan.Back to all posts
I’ve lived this migration on real apps—airport kiosks that must work offline, telecom analytics with spiky websocket traffic, and enterprise dashboards with PrimeNG tables that can repaint a page to death. The pattern that wins isn’t “rip out zone.js.” It’s a phased Signals + SignalStore rollout with metrics and feature flags, then a controlled move toward zoneless when your UX is proven stable.
Why Ripping Out zone.js Breaks Enterprise UX
What teams try first (and why it hurts)
I’ve audited teams that toggled ngZone:'noop' on day one. It works on toy routes, then PrimeNG dialogs stop opening, Stripe widgets don’t update, and user typing stalls change detection. Without a plan, you trade CPU savings for angry users.
Turn on ngZone:'noop' globally
Hope signals or async pipes keep things updating
Discover file uploads, dialogs, and 3rd‑party widgets no longer repaint
Why signals alone aren’t a silver bullet
Signals are deterministic and fast, but external libraries, timers, and websockets still need a path to mark views. Until your orchestration is signal‑first and your boundaries are fenced, zoneless is fragile.
Signals update consumers, not arbitrary 3rd‑party code
Legacy services emit RxJS side‑effects outside Angular’s awareness
A Multi‑Phase Plan: From Zone‑Coalesced to Zoneless with Signals
Phase 0 — Baseline metrics and guardrails
As companies plan 2025 Angular roadmaps, grab a performance snapshot before touching code. I record INP/LCP, render counts from Angular DevTools flame charts, and interaction timings on critical flows (auth, search, table filter, checkout). Set budgets in CI so regressions fail PRs.
INP/LCP via Lighthouse and GA4
Angular DevTools render counts on worst routes
CI smoke tests for dialogs/forms/tables
Phase 1 — Signals in leaf components (zone still on)
Start where blast radius is tiny: leaf components. Turn @Input into input() signals, fold local UI flags into signal state, and compute derived values. You’ll see fewer template recalcs even with zone still firing.
Use input() signal inputs and computed() for derived UI
Replace local Subjects with signal state
Keep outputs with output() or EventEmitter
Phase 2 — Introduce SignalStore for orchestration
Push orchestration into a SignalStore per domain (auth, filters, selection). It centralizes derivations, memoizes joins, and makes effects explicit—perfect for later zoneless change detection.
Adopt @ngrx/signals signalStore
Keep services thin: IO only
Move derivations/joins into withComputed
Phase 3 — Bridge RxJS and websockets with toSignal/effect
I’ve migrated ad analytics dashboards that consumed typed websocket events. We wrap streams with toSignal, feed stores, and replace scattered async pipes with computed signals—cutting renders by 30–55% before touching zone.
Use toSignal for live streams
Persist/side‑effect with effect()
Remove redundant async pipe churn
Phase 4 — Fence third‑party widgets
PrimeNG, Stripe, and map libraries can change state outside Angular. Boundary components let you explicitly re‑enter the zone (or just mark the view) without waking the entire tree.
Wrap heavy widgets in a boundary component
Use NgZone.run(...) for callbacks
markForCheck() where necessary
Phase 5 — Preview a zoneless build behind a flag
Only after stores and boundaries are in place do we try zoneless in a preview channel. We gate bootstrap with an environment flag, run Cypress + Lighthouse, then send real traffic incrementally. Production stays on zone until metrics prove parity.
Enable ngZone:'noop' only in preview/staging
Gate with an env flag or Remote Config
Watch INP, error rates, and UX flows
Phase 6 — Production cutover with rollback
If metrics hold (no INP spikes, stable render counts, consistent error rate), flip the flag in production. Keep the ability to revert within minutes. This is how we avoided kiosk regressions for a major airline and kept dashboards stable for a telecom provider.
Feature flag the switch
Zero‑downtime deploy
Rollback in one toggle
SignalStore Pattern: Bridging RxJS to Zoneless
A production‑ready store skeleton
Here’s a trimmed SignalStore used on a real telemetry dashboard. It bridges a typed websocket stream with toSignal, derives view state with computed, and isolates side‑effects with effect.
Code
import { computed, effect, inject, Signal } from '@angular/core';
import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
import { toSignal } from '@angular/core/rxjs-interop';
import { TelemetryService, TelemetryEvent } from './telemetry.service';
interface TelemetryState {
events: TelemetryEvent[];
filter: 'all' | 'errors' | 'warn';
connected: boolean;
}
const initial: TelemetryState = {
events: [],
filter: 'all',
connected: false,
};
export const TelemetryStore = signalStore(
{ providedIn: 'root' },
withState(initial),
withComputed((state) => ({
filtered: computed(() => {
const f = state.filter();
const list = state.events();
return f === 'all' ? list : list.filter(e => e.level === (f === 'errors' ? 'error' : 'warn'));
}),
errorCount: computed(() => state.events().filter(e => e.level === 'error').length),
})),
withMethods((state) => {
const svc = inject(TelemetryService);
const stream: Signal<TelemetryEvent | null> = toSignal(svc.events$, { initialValue: null });
// Pull events from RxJS into signals
effect(() => {
const evt = stream();
if (!evt) return;
patchState(state, { events: [...state.events(), evt] });
});
// External side-effect with explicit wiring
effect(() => {
if (state.errorCount() > 100) {
svc.raiseAlarm();
}
});
return {
setFilter: (f: TelemetryState['filter']) => patchState(state, { filter: f }),
connect: () => svc.connect(),
disconnect: () => svc.disconnect(),
};
})
);Why this works zoneless
When ngZone:'noop' is enabled, signal updates still notify consumers. With derivations in computed and view reads bound to the store, you avoid the need for zone‑triggered checks.
All view derivations live in computed signals
External IO is explicit in effects
No reliance on zone for propagation
PrimeNG Boundary Component and Change Detection Fencing
Boundary strategy
Instead of sprinkling detectChanges everywhere, fence high‑risk widgets. Re‑enter Angular when callbacks fire, then mark for check. This keeps CPU low and UX predictable.
Keep templating ergonomic; isolate unknowns
Use ChangeDetectorRef.markForCheck() as the contract
Code
import { Component, ChangeDetectionStrategy, ChangeDetectorRef, NgZone, input } from '@angular/core';
import { TelemetryStore } from './telemetry.store';
@Component({
selector: 'app-events-table',
template: `
<p-table [value]="store.filtered()" [rows]="25" [paginator]="true" [trackBy]="trackById">
<ng-template pTemplate="header">
<tr>
<th>Time</th>
<th>Level</th>
<th>Message</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-row>
<tr>
<td>{{ row.time }}</td>
<td>{{ row.level }}</td>
<td>{{ row.message }}</td>
</tr>
</ng-template>
</p-table>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EventsTableComponent {
store = inject(TelemetryStore);
constructor(private cdr: ChangeDetectorRef, private zone: NgZone) {}
// Example: PrimeNG callback from outside Angular
onLazyLoad(e: any) {
this.zone.run(() => {
this.store.setFilter('all');
this.cdr.markForCheck();
});
}
trackById = (_: number, row: any) => row.id;
}Result
In a telecom dashboard, this cut table rerenders from 12→5 per interaction and eliminated ‘stale row’ bugs during high‑frequency websocket bursts.
Fewer global checks
Predictable updates even zoneless
Toggle Zoneless Safely with Feature Flags
Bootstrap gating
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideZoneChangeDetection } from '@angular/core';
import { environment } from './environments/environment';
bootstrapApplication(AppComponent, {
providers: [
provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }),
],
// Enable zoneless only in preview builds
ngZone: environment.flags.zoneless ? 'noop' : undefined,
});CI guardrails
# .github/workflows/ci.yaml
name: ci
on: [push, pull_request]
jobs:
test-web:
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 -- --configuration=preview-zoneless
- run: npm run test -- --watch=false
- run: npm run e2e:headless # Cypress smoke for dialogs/forms/tables
- run: npm run lh:ci # Lighthouse budgets guard INP/LCPPreview traffic and rollback
With Nx + Firebase Hosting previews, we route a small cohort to the zoneless build. If INP or error rates drift, we cut back instantly. This mirrors how gitPlumbers maintains 99.98% uptime during complex modernizations.
Ship to Firebase preview channels
Flip the env flag remotely
Rollback in minutes
When to Hire an Angular Developer for Legacy Rescue
Signals migration triggers that justify help
If your app fits these, bring in a senior Angular consultant. I’ve stabilized employee tracking portals at a global entertainment company and kiosk flows for a major airline with the approach above—without halting delivery.
AngularJS/early Angular code with custom change detection hacks
Critical PrimeNG/Material pages jitter or freeze under load
RxJS spaghetti makes state unpredictable
How an Angular Consultant Approaches Signals Migration
My 2–6 week playbook
Typical engagements start with flame charts and GA4. We pick one revenue‑critical route, migrate to SignalStore, fence third‑party widgets, then trial zoneless in a preview build. Delivery continues—no big‑bang rewrites.
Week 1: Audit, metrics, risk map, and pilot route
Week 2–3: SignalStore + boundaries on hot paths
Week 4+: Preview zoneless, CI budgets, and rollout
What you get
You’ll leave with measurable wins and a roadmap your team can execute. If you need a remote Angular developer to keep momentum, I can stay on as a fractional Angular architect.
Before/after metrics and a rollback plan
Refactor PRs with tests and docs
A Signals playbook your team can own
Concise Takeaways and Next Steps
The playbook in one minute
If you need help, hire an Angular developer who has shipped this at scale. I’m available for selective Angular 20+ migrations and dashboard rescues.
Adopt Signals/SignalStore first with zone on
Fence 3rd‑party widgets and bridge RxJS
Gate ngZone:'noop' behind flags and CI
Cut over only after metrics prove parity
FAQs: Signals, Zoneless, and Migration Timelines
Quick answers
See common questions below. If you need specifics for your codebase, let’s discuss your Angular project and I’ll share a tailored plan within a week.
Key takeaways
- Don’t remove zone.js first. Adopt Signals/SignalStore with zone still on, measure, then carve out zoneless islands.
- Use toSignal + effects to bridge RxJS/websockets while you transition.
- Fence third‑party UI (e.g., PrimeNG) with NgZone.run(...) and ChangeDetectorRef.markForCheck when exploring zoneless.
- Gate the final ngZone:'noop' switch behind an environment flag and test in preview channels first.
- Instrument render counts, INP/LCP, and error rates; block regressions in CI before rollout.
Implementation checklist
- Baseline performance (INP/LCP, render counts) and critical flows.
- Introduce Signals in leaf components with zone enabled.
- Move orchestration to SignalStore; keep services thin.
- Bridge RxJS with toSignal and effects; remove redundant async pipes.
- Isolate 3rd‑party widgets and fence change detection.
- Gate a zoneless build (ngZone:'noop') behind feature flags for preview only.
- Run CI guardrails: Cypress smoke, Lighthouse budgets, Pa11y/axe.
- Cut over to zoneless after error‑free preview traffic and stable metrics.
Questions we hear from teams
- How long does a zone.js → Signals/SignalStore migration take?
- For a focused dashboard, 2–4 weeks for the first feature set; 4–8 weeks for full app coverage. We start with one route, ship metrics, then expand. Zoneless is trialed in preview channels before production.
- Will PrimeNG, Stripe, or map widgets work without zone.js?
- Yes—with boundaries. Wrap them in a component that re-enters Angular via NgZone.run and markForCheck. Test in a preview build before flipping ngZone:'noop' globally.
- What does an Angular consultant actually deliver here?
- Audit + plan, SignalStore architecture, boundary components, CI guardrails (Cypress/Lighthouse), and a gated zoneless rollout. You get PRs, docs, and measurable performance improvements with rollback options.
- How much does it cost to hire an Angular developer for this?
- Varies by scope. Typical rescue/migration engagements start at 2–6 weeks. I offer fixed-scope pilots for a single critical route so you can see results and de-risk a longer engagement.
- Do we need to rewrite our NgRx or RxJS code?
- No. Bridge existing streams with toSignal and migrate progressively. Keep reducers/effects that still add value; move UI derivations to computed signals for deterministic updates.
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