Don’t Flip the Switch: A Safe, Multi‑Phase Plan to Migrate From zone.js to Signals + SignalStore in Angular 20+

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/LCP

Preview 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.

Related Resources

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.

Hire Matthew — Remote Angular Expert, Available Now See How I Rescue Chaotic Codebases (gitPlumbers)

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