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