Plan a Multi‑Phase Migration from zone.js to Signals + SignalStore in Angular 20—Without Breaking UX

Plan a Multi‑Phase Migration from zone.js to Signals + SignalStore in Angular 20—Without Breaking UX

A pragmatic, metrics‑driven path to go zoneless in Angular 20+: phase gates, SignalStore, adapters for legacy code, and CI guardrails that keep production calm.

“Don’t flip zoneless first. Teach your app to speak Signals, then remove zone.js like a cast after the bone has healed.”
Back to all posts

I’ve broken production so you don’t have to. The first time I took a large Angular dashboard zoneless, a PrimeNG table stopped updating after a WebSocket burst. The root cause wasn’t Angular—it was our own NgZone/detectChanges band‑aids from years of patching. Here’s the plan I use now on Fortune 100 apps to move from zone.js change detection to Signals + SignalStore without jitter, regressions, or late‑night rollbacks.

The dashboard stops jittering, then freezes: the realities of going zoneless

The scene

It’s Monday. Your real‑time analytics board (telecom ad spend, 30k events/min) jitters less after early Signals refactors. Product smiles. Then you flip zoneless in a staging build—some widgets don’t update. Why? Imperative change‑detection hacks and third‑party assumptions about zone.js.

2025 context

As teams plan 2025 Angular roadmaps, zoneless + Signals is the biggest maintainability and performance unlock I’m seeing across dashboards, kiosks, and multi‑tenant portals. But you must phase it.

  • Angular 20+ makes zoneless stable and first‑class.

  • Hiring cycles pick up in Q1—leadership asks for hard numbers.

Why Angular apps break during zone.js → Signals migration

Imperative patches

Those work under zone.js because the microtask queue frequently nudges views. Zoneless removes that crutch; now only signals advance the UI.

  • ChangeDetectorRef.detectChanges sprinkled in services/components

  • NgZone.run used as a catch‑all for async callbacks

Third‑party assumptions

Libraries like PrimeNG are largely fine; the issues are almost always our in‑house wrappers that mutated inputs instead of emitting signals.

  • Legacy wrappers around Datepickers/Charts that call markForCheck

  • Direct mutation of Input objects (without a signal boundary)

Mixed async sources

Zoneless expects side effects to be modeled as effects and state changes to come through signals/SignalStore. When arrays are mutated in place, computed signals never re‑run.

  • RxJS streams not bridged into signals

  • WebSocket burst handlers mutating arrays in place

Plan a multi‑phase zone.js → Signals + SignalStore migration

Here’s how I wire the flag and the providers.

Phase 0: Instrument and freeze the blast radius

Before touching code, measure. I log DevTools cycles while replaying telemetry via a script, then pin thresholds in CI.

  • Baseline Angular DevTools change‑detection cycles on key routes

  • Capture Core Web Vitals (LCP/INP/CLS) with GA4 or Firebase Analytics

  • Add feature flag for zoneless builds

Phase 1: Signals under zone.js (no UX risk)

This builds the muscle without changing global detection. Most apps see 15–30% fewer view updates right here.

  • Convert high‑churn components to signal inputs/computed/effects

  • Bridge RxJS streams with toSignal

  • Stop mutating arrays/objects in place—use immutable updates

Phase 2: Introduce SignalStore facades

SignalStore gives you a single reactive surface for the UI to depend on. You can still feed it from NgRx or services during the transition.

  • Create store per domain (user, devices, analytics)

  • Expose readonly computed selectors + method updaters

  • Keep existing NgRx/services as the backing data source temporarily

Phase 3: Remove imperative patches

If a view depends on something, make it a signal. If something causes a side effect (e.g., analytics log), make it an effect.

  • Delete detectChanges/markForCheck scattered calls

  • Replace NgZone.run with signal updates and effects

Phase 4: Staging canary with zoneless

If a widget stalls, you’ll find an un‑signaled state mutation or a wrapper that assumed zones. Fix there; do not revert the approach.

  • Enable provideZonelessChangeDetection in staging build only

  • Run Cypress e2e + Lighthouse; diff DevTools cycles against baseline

Phase 5: Progressive production rollout

We’ve used the same pattern for airport kiosk apps and telecom dashboards—no after‑hours heroics required.

  • Firebase Hosting preview channels for QA sign‑off

  • 1–5% canary, watch error budgets and INP

  • Ramp to 100% once stable

Phase 6: Retire zone.js + document patterns

Your future hires will thank you. And yes, recruiters love seeing clear Signals docs in repos.

  • Remove zone.js polyfill and clean config

  • Add ADRs and docs for Signals/SignalStore conventions

Bootstrap and config snippets you can drop in today

main.ts with zoneless flag

import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router';
import { provideZonelessChangeDetection } from '@angular/core';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
import { environment } from './environments/environment';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(),
    provideRouter(routes),
    ...(environment.enableZoneless ? [provideZonelessChangeDetection()] : [])
  ]
});

Polyfills cleanup when going live

// polyfills.ts
// Remove: import 'zone.js'; // ❌ no longer needed when zoneless

CI guardrail for a staged flip

# .github/workflows/e2e.yml
name: e2e-zoneless
on: [push]
jobs:
  test:
    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=staging --define=enableZoneless=true
      - run: npx cypress run --config-file cypress.zoneless.config.ts
      - run: npx lighthouse http://localhost:4200 --budget-path=./budgets.json

SignalStore facade pattern over legacy state

This facade lets you keep old NgRx slices alive while the UI migrates. Later, you can retire reducers by moving their logic into store methods.

Store definition

// devices.store.ts
import { signalStore, withState, withMethods, patchState } from '@ngrx/signals';
import { computed, inject } from '@angular/core';
import { DevicesApi } from './devices.api';

interface DevicesState {
  list: ReadonlyArray<Device>;
  selectedId: string | null;
  loading: boolean;
}

const initialState: DevicesState = { list: [], selectedId: null, loading: false };

export const DevicesStore = signalStore(
  withState(initialState),
  withMethods((store) => {
    const api = inject(DevicesApi);

    const load = async () => {
      patchState(store, { loading: true });
      const data = await api.fetchDevices();
      patchState(store, { list: data, loading: false });
    };

    const select = (id: string | null) => patchState(store, { selectedId: id });

    return { load, select };
  })
);

// selectors (signals)
export const devices = (s: DevicesState) => s.list;
export const selected = (s: DevicesState) =>
  computed(() => s.list.find(d => d.id === s.selectedId) ?? null);

Component usage with Signals

@Component({
  selector: 'ux-device-panel',
  template: `
    <p-table [value]="store.list()" (onRowSelect)="store.select($event.data.id)"></p-table>
    <ux-device-details [device]="selected()" />
  `
})
export class DevicePanel {
  store = inject(DevicesStore);
  readonly selected = selected(this.store.state());
  ngOnInit() { this.store.load(); }
}

Bridging RxJS feeds

// If you have a WebSocket stream
import { toSignal } from '@angular/core/rxjs';
readonly deviceEvents = toSignal(this.socket.events$, { initialValue: null });

// React via effect (no NgZone.run required)
effect(() => {
  const evt = this.deviceEvents();
  if (!evt) return;
  this.store.applyEvent(evt); // method updates signals under the hood
});

Example: migrating a PrimeNG DataTable filter to Signals

Under zoneless, this continues to work—no change detection calls, no NgZone. Angular DevTools will show far fewer view updates during fast typing.

Before (zone.js + mutable state)

filterText = '';
rows: Order[] = [];

applyFilter(txt: string) {
  this.filterText = txt;
  this.cd.detectChanges(); // ❌ imperative
}

After (Signals + computed)

import { input, signal, computed, effect } from '@angular/core';

readonly filter = signal('');
readonly rows = signal<Order[]>([]);
readonly filtered = computed(() => {
  const f = this.filter().toLowerCase();
  return this.rows().filter(r => r.customer.toLowerCase().includes(f));
});

// template
// <input pInputText (input)="filter.set($any($event.target).value)"/>
// <p-table [value]="filtered()"></p-table>

// optional analytics without zone
effect(() => {
  this.analytics.track('orders_filter', { q: this.filter() });
});

How an Angular consultant approaches Signals migration

If you need to hire an Angular developer or engage a senior Angular expert to lead this work, I can run the plan end‑to‑end with your team—remote, hands‑on, and metrics‑driven.

Discovery (week 1)

I ship a written assessment with a prioritized migration map and risk register.

  • Audit high‑churn components via DevTools

  • Map NgZone/detectChanges hotspots

  • Baseline Web Vitals in CI

Implementation (weeks 2–6)

I keep feature delivery moving; no feature freeze required.

  • Convert components to Signals

  • Introduce SignalStore facades

  • Fix adapters around tables/charts/forms

  • Zoneless canary in staging → production

Outcomes I target

On a telecom analytics app, typed WebSockets + SignalStore cut dropped frames to zero and held 60fps during peak bursts.

  • −40–70% change‑detection cycles on key routes

  • Stable INP even under WebSocket bursts

  • Zero user-visible regressions during rollout

When to Hire an Angular Developer for Legacy Rescue

Signals that you need help

This is classic pre‑zoneless pain. Bringing in an Angular contractor for a focused 2–4 week rescue pays for itself in stability alone.

  • Frequent detectChanges/Zones patches in code reviews

  • Flaky tables/forms after async events

  • Multiple state sources with unclear ownership

What you’ll get

You keep shipping features while the foundation gets stronger.

  • A phased plan with roll‑back levers

  • SignalStore facades for critical domains

  • CI guardrails and documentation

Takeaways + what to instrument next

If you want a second set of eyes on your roadmap—or need an Angular consultant to lead the migration—let’s talk. I’m currently accepting 1–2 projects per quarter.

Measure, migrate, verify, then flip

You don’t need heroics—just a disciplined plan.

  • Instrument before code changes

  • Migrate components + state first

  • Kill imperative patches

  • Canary zoneless with guardrails

Next steps to instrument

I keep these dashboards visible to PMs so we make business‑visible progress every sprint.

  • Angular DevTools profiles saved per route

  • GA4/Firebase custom metrics for INP under interaction types

  • Feature flags tied to release channels

Related Resources

Key takeaways

  • You don’t flip a global zoneless switch first—you earn it by migrating components and state to Signals incrementally under zone.js.
  • Phase‑gate the rollout: instrument first, then convert component state, add SignalStore facades, canary zoneless in staging, then retire NgZone hacks.
  • Adapters are your friend: toSignal for RxJS, SignalStore for domain state, thin wrappers for third‑party components that assumed zone.js.
  • Guard the UX with metrics: Angular DevTools cycles, Core Web Vitals, and visual regression in CI (Storybook/Chromatic optional) before and after each phase.
  • Roll out with feature flags and Firebase Hosting previews; if canary fails, revert by config—no code fork required.

Implementation checklist

  • Instrument DevTools + Lighthouse baselines before any migration.
  • Convert high‑churn components to Signals (inputs/computed/effects) under zone.js.
  • Introduce SignalStore facades over existing services/NgRx slices.
  • Kill brittle NgZone/detectChanges hacks and replace with signal‑driven flows.
  • Run zoneless in staging behind a build flag; execute e2e and smoke tests.
  • Canary release to <10% traffic; monitor INP/LCP and error budgets.
  • Remove zone.js, clean polyfills, and document new patterns in ADRs.

Questions we hear from teams

How long does a zone.js → Signals migration take?
For a mid‑size dashboard, expect 2–3 weeks to instrument and convert critical components, another 1–2 weeks to add SignalStore facades, and 1 week for zoneless canary and rollout. Larger multi‑tenant apps run 6–8 weeks with parallel feature delivery.
Will PrimeNG or charts break without zone.js?
PrimeNG generally works fine. Issues arise in custom wrappers that mutate inputs or rely on detectChanges. Replace those with Signals and computed state. For charts (Highcharts/D3), update via signal setters instead of imperative redraw calls.
Do we need to rewrite NgRx to use SignalStore?
No. Introduce SignalStore as a facade and feed it from existing selectors/effects. Move logic incrementally into store methods. You can retire NgRx slices gradually without a risky flag day.
What does an Angular consultant do on this engagement?
I map hotspots, set up guardrails, convert high‑churn components to Signals, introduce SignalStore, and supervise the zoneless rollout. You get a phased plan, code reviews, CI checks, and documentation your team can sustain.
How much does it cost to hire an Angular developer for this?
Typical rescue/migration engagements run 2–8 weeks. I offer fixed‑scope assessments and weekly rates for implementation. Discovery call within 48 hours; written plan within 5 business days after access.

Ready to level up your Angular experience?

Let AngularUX review your Signals roadmap, design system, or SSR deployment plan.

Hire Matthew – Remote Angular Expert (Signals + Zoneless) Need a code rescue first? See how I stabilize chaotic apps

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
NG Wave Component Library

Related resources