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

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

A pragmatic, phased path to zoneless Angular: feature flags, SignalStore, RxJS interop, and CI guardrails that keep dashboards stable while you modernize.

Cut zone churn first, flip zoneless last. The flag is a release, not a refactor.
Back to all posts

I’ve moved large Angular apps to Signals + SignalStore without stopping feature delivery—airport kiosks, an ads analytics dashboard for a telecom provider, and a multi‑tenant scheduler at a broadcast network. The secret is sequencing: migrate state and rendering patterns first, flip zoneless last—behind a flag with measurable guardrails.

If you need a senior Angular engineer to plan or lead this, I’m a remote Angular consultant with 10+ years in enterprise dashboards, telemetry, and upgrades. This is the playbook I use on Angular 20+ with Nx, PrimeNG, Firebase, and CI you can trust.

A dashboard jitters, the exec wants numbers, you need a plan

You know the smell: a PrimeNG dashboard that jitters when filters change, components re-render on scroll, and every microtask pings the zone bell. Meanwhile leadership wants proof that Signals will reduce cost-to-ship and improve Core Web Vitals before Q1. I’ve been there—on employee tracking systems at a global entertainment company and on a broadcast VPS scheduler. The move to Signals + SignalStore is worth it, but only if you plan it like a release, not a refactor.

Why Angular 20+ teams should go zoneless in phases

Determinism beats magic

With Signals, rendering is explicit and predictable. That’s gold for SSR/hydration and for tests. SignalStore (NgRx signals) gives you a minimal, composable state layer with derived state via computed() and safe mutations.

  • Signals make dependency graphs explicit

  • Effects localize side effects

  • SignalStore concentrates business rules

Performance you can prove

On a telecom ads analytics platform, moving key tables and filters to Signals cut render counts by 60% and stabilized INP spikes caused by zone churn. Executives care because you can show the numbers.

  • Fewer re-renders and smaller change detection surfaces

  • Better INP/LCP from fewer long tasks

  • Less coupling to zone microtasks

Risk is in the switch, not the store

If you move to zoneless before your state is signal-driven, components depend on zone magic to update. Do the inverse: push state into Signals/SignalStore first, then flip ngZone behind a flag.

  • Most UX breakage happens when changing ngZone

  • Migrate state first; flip zoneless when the app is already signal-friendly

The multi‑phase migration roadmap

# ci/lighthouse-budget.yml (Nx + GitHub Actions)
name: lighthouse
on: [pull_request]
jobs:
  lhci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
        with: { version: 9 }
      - run: pnpm install --frozen-lockfile
      - run: pnpm nx run web:build:production
      - run: npx @lhci/cli autorun --upload.target=temporary-public-storage
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}

Phase 1 — Migrate leaf components to Signals

  • Convert @Input to input(), derive with computed(), and move template pipes into computed() functions. Start where change cascades are worst (grids, filters, charts).

// leaf-filter.component.ts
import { Component, input, computed, effect, signal } from '@angular/core';

@Component({
  selector: 'leaf-filter',
  standalone: true,
  template: `
    <p-dropdown [options]="options()" [(ngModel)]="selected()"></p-dropdown>
  `
})
export class LeafFilterComponent {
  options = input.required<{ label: string; value: string }[]>();
  selected = signal<string | null>(null);

  // derive expensive work once per change, not per CD tick
  selectedLabel = computed(() =>
    this.options().find(o => o.value === this.selected() ?? '')?.label ?? 'All'
  );

  // local side effect: analytics
  log = effect(() => {
    console.debug('filter selected', this.selectedLabel());
  });
}

Phase 2 — Introduce SignalStore per feature

  • Create small stores for cohesive features (auth, filters, telematics devices). Keep fetch logic and derived state in the store; UI consumes signals.

// devices.store.ts (NgRx SignalStore)
import { SignalStore, withEntities, patchState, withComputed } from '@ngrx/signals';
import { computed, inject } from '@angular/core';
import { DevicesApi, Device } from '../data/devices.api';

export class DevicesStore extends SignalStore(
  { entities: withEntities<Device>() },
  withComputed(({ entities }) => ({
    onlineCount: computed(() => entities().filter(d => d.online).length),
  }))
) {
  private api = inject(DevicesApi);

  load() {
    this.api.list().subscribe(list => patchState(this, state => ({
      entities: list
    })));
  }

  toggle(id: string) {
    patchState(this, ({ entities }) => ({
      entities: entities.map(d => d.id === id ? { ...d, online: !d.online } : d)
    }));
  }
}

Phase 3 — Typed RxJS↔Signals adapters

  • Bridge HTTP/WebSockets to Signals with toSignal; expose readonly signals to components. Keep stream types strict and testable.

// rxjs-adapter.ts
import { toSignal } from '@angular/core/rxjs-interop';
import { Injectable, computed, effect } from '@angular/core';
import { map, shareReplay } from 'rxjs/operators';
import { TelemetryService } from './telemetry.service';

@Injectable({ providedIn: 'root' })
export class VehicleTelemetryState {
  private $events = this.telemetry.events$.pipe(
    map(e => ({ id: e.id, speed: e.speed, ts: e.ts })),
    shareReplay({ bufferSize: 1, refCount: true })
  );

  events = toSignal(this.$events, { initialValue: [] as any[] });
  avgSpeed = computed(() => {
    const list = this.events();
    return list.length ? list.reduce((a, b) => a + b.speed, 0) / list.length : 0;
  });

  constructor(private telemetry: TelemetryService) {
    effect(() => {
      // example side effect: alert if average exceeds threshold
      if (this.avgSpeed() > 80) console.warn('High average speed');
    });
  }
}

Phase 4 — Zoneless opt‑in behind a release flag

  • Zoneless is an application-level switch. Treat it like a release: enable on a preview channel, then canary, then 100%.

// main.ts — choose zoneless at bootstrap via env flag (set per deploy channel)
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app/app.routes';
import { provideRouter } from '@angular/router';

// window.__APP_FLAGS__ injected at build/deploy time (e.g., Firebase Hosting channel)
const flags = (window as any).__APP_FLAGS__ ?? { zoneless: false };

bootstrapApplication(AppComponent, {
  providers: [provideHttpClient(), provideRouter(routes)],
  ngZone: flags.zoneless ? 'noop' : 'zone.js'
}).catch(console.error);

Phase 5 — Remove zone dependencies and document patterns

  • Replace any imperative markForCheck patterns that depended on zone microtasks. Audit third-party libs (date pickers, charts). PrimeNG and Highcharts work fine with Signals if inputs are signals/computed and change events are explicit.

  • Update team docs, add examples in Storybook, and enforce with lint rules (no @Input set with heavy pipe chains in templates, prefer computed()).

Phase 0 — Baseline and guardrails

Before touching code, capture baselines. Use Angular DevTools to measure render counts for critical views. Record flame charts for filter interactions. Ship web-vitals to GA4/BigQuery. Add Storybook with Chromatic checks to catch CSS regressions—especially if you use PrimeNG theming. Finally, add CI budgets so we fail fast if performance regresses.

  • Render counts with Angular DevTools

  • Flame charts for hot paths

  • GA4/BigQuery UX timings (LCP/INP/TTFB)

  • Storybook a11y + visual regression

  • Lighthouse budgets in CI

CI example — Lighthouse and bundle budgets

Real‑world guardrails from the field

Airport kiosks (offline‑first)

On a major airline kiosk, we used Docker to simulate peripherals and Signals to drive device state indicators. Zoneless reduced microtask churn when devices flapped offline/online, and SignalStore made retry flows explicit and testable.

  • Docker-based device simulation

  • Peripheral APIs: scanners, printers, card readers

  • Zoneless stabilized error recovery loops

Telecom analytics dashboard

We migrated filters, KPI tiles, and detail drawers to Signals/SignalStore, then flipped zoneless behind a preview flag. Render counts on the main grid dropped ~55%, INP spikes vanished, and executives finally had numbers they trusted.

  • PrimeNG data virtualization

  • Typed WebSocket events

  • Core Web Vitals improved across the board

How an Angular Consultant Approaches Signals Migration

Discovery (week 1)

I start with an assessment: where zone is masking state smells, which features stress the change detector, and which PrimeNG/Material components are hot. You get a written plan, estimates, and a safety net for rollout.

  • Repo review (Nx graph, dependency-cruiser)

  • Perf baselines (DevTools, GA4)

  • Risk map (zone touchpoints)

Implementation (2–6 weeks)

We migrate leaf components, introduce SignalStore, adapt RxJS streams, then flip zoneless on staging. Each step ships, measurable. Teams learn by pairing so the patterns stick.

  • Phase-by-phase PRs with flags

  • CI guardrails and dashboards

  • No feature freeze

Rollout + enablement

We use Firebase Hosting preview channels and feature flags per environment to control exposure. Post-rollout, your team owns the patterns with docs and Storybook examples.

  • Preview channels / canary

  • Runbooks + playbooks

  • Knowledge transfer

When to Hire an Angular Developer for Legacy Rescue

Signals are promised, but delivery is jittery

If your Angular 12–18 app is thrashing or tests are flaky, bring in a senior Angular engineer to map a safe migration. I’ve rescued AngularJS→Angular rewrites, zone-heavy code, and even legacy JSP frontends.

  • Render thrash on user input

  • SSR hydration mismatches

  • Intermittent test flakes

You need measurable outcomes, fast

Leaders want proof. We’ll show flame charts, render counts, and Core Web Vitals trending up—without pausing roadmap delivery.

  • Board-ready numbers

  • No feature freeze

  • Zero-downtime deploys

Common pitfalls and how to avoid them

Flipping zoneless too early

Zoneless first feels tempting; it’s risky. Make the app signal-friendly, then switch.

  • Keep zone until Signals are pervasive

  • Use flags + preview channels

Leaky effects

Effects that call APIs from components grow messy. Centralize in SignalStore or services with typed adapters.

  • Co-locate effects with stores

  • Cleanup logic in services, not components

Overusing computed in templates

Prefer computed() in TypeScript and bind the result. Keep templates dumb.

  • Compute in TS, bind signals in HTML

  • Avoid expensive pipes in templates

Measurable outcomes and what to instrument next

What success looks like

You should see fewer render passes on key routes, elimination of zone-driven jitter, improved INP, and stable SSR hydration. Log these wins in GA4/BigQuery and include in your engineering readout.

  • 40–70% fewer renders on hot routes

  • Reduced INP/LCP and fewer long tasks

  • Deterministic tests and SSR hydration

What to instrument next

Add Angular DevTools render counters in dev and ship user timing marks for search, checkout, and export flows. Tie rollout flags to telemetry so you can correlate zoneless enablement with UX gains.

  • Component render counters

  • User timing marks for key flows

  • Feature flags telemetry

Related Resources

Key takeaways

  • Treat zoneless as a release switch, not a one‑day refactor. Move state to Signals/SignalStore first, flip ngZone last.
  • Baseline metrics before touching code: render counts, flame charts, Core Web Vitals, and UX timing in GA4/BigQuery.
  • Start at the edges: migrate leaf components to Signals and input() to reduce render cascades.
  • Introduce SignalStore per feature to own data, derived state, and side effects; use typed adapters between RxJS and Signals.
  • Gate the zoneless switch behind a release flag and progressive rollout (preview channels, canary users).
  • Add CI guardrails: Lighthouse budgets, render-count thresholds, Storybook a11y/visual checks to prevent regressions.

Implementation checklist

  • Capture baselines: Angular DevTools render counts, flame charts, LCP/INP, UX timings.
  • Add CI budgets and Storybook/Chromatic guards for a11y and visuals.
  • Migrate leaf components to Signals: input(), computed(), effect().
  • Create per‑feature SignalStores; centralize derived state and mutations.
  • Bridge RxJS→Signals with typed toSignal/fromSignal adapters.
  • Flip ngZone: 'noop' behind a release flag on staging; fix edge cases.
  • Progressively roll out zoneless via preview channels/canary traffic.
  • Remove zone dependencies, finalize documentation and team patterns.

Questions we hear from teams

How long does an Angular zoneless migration take?
For a typical enterprise app, plan 2–4 weeks for assessment and leaf-component migrations, 2–6 weeks to introduce SignalStore and RxJS adapters, and 1 week to flip zoneless with canary rollout. No feature freeze required.
Do we need NgRx if we use SignalStore?
SignalStore is part of NgRx’s signals suite. You can use it standalone without reducers/effects. For complex cross-feature orchestration, classic NgRx still pairs well with Signals for predictable event flows.
What breaks when turning off zone.js?
Anything relying on zone microtasks to trigger change detection. Replace those with signals, computed(), and explicit events. Audit third‑party controls; PrimeNG/Material work fine when inputs are signals and updates are explicit.
How much does it cost to hire an Angular developer for this?
Engagements vary by size. Typical migrations run 4–8 weeks. I offer fixed-scope assessments and phase-based delivery. Contact me to scope your codebase and get a tailored estimate within a week.
What’s included in a typical engagement?
Assessment report, phased migration plan, SignalStore setup, RxJS→Signals adapters, CI guardrails, rollout strategy, and knowledge transfer. Discovery call within 48 hours; initial assessment delivered in 5–7 days.

Ready to level up your Angular experience?

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

Hire Matthew — Remote Angular Expert for Signals Migration See live Angular apps and components (NG Wave, IntegrityLens, SageStepper)

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