Zero‑Downtime Angular 11 → 20: Service Worker Traps, TS5 “bundler”, RxJS 8—and a 31% Faster LCP

Zero‑Downtime Angular 11 → 20: Service Worker Traps, TS5 “bundler”, RxJS 8—and a 31% Faster LCP

How I upgraded a live analytics dashboard from Angular 11 to 20 in six weeks with no outages: week‑by‑week timeline, the breaking changes that matter, and the performance wins you can bank.

“Zero downtime isn’t magic—it’s guardrails, canaries, and changing one risky thing at a time.”
Back to all posts

Challenge

Ad‑ops at a leading telecom provider lives inside an Angular dashboard that feeds on WebSockets and scheduled jobs. Traffic is 24/7 and the SLA doesn’t allow even a five‑minute hiccup. This app was still on Angular 11 with TSLint, RxJS 6 patterns, and a themed PrimeNG stack. The ask: upgrade to Angular 20+ with measurable performance gains—without any downtime.

Intervention

I ran a six‑week upgrade with Nx, cookie‑based canaries on Firebase Hosting, and a disciplined hop‑by‑hop ng update path. We introduced Signals and SignalStore only in hot components, migrated to TypeScript 5’s moduleResolution "bundler", and cleaned up RxJS to v8.

Result

Zero incidents in production, -36% JS delivered on the home workflow, -31% median LCP, -27% INP. Lighthouse Perf improved from 83 → 98 on the most trafficked report. If you need to hire an Angular developer or an Angular consultant for a similar modernization, this is the playbook I use.

Why this matters for 2025 roadmaps and SLAs

Angular 20+ expectations

With Angular 21 beta arriving soon, teams planning 2025 roadmaps want the performance and ergonomics of Angular 20+ without the risk. Your execs care about Core Web Vitals, not change logs. Upgrading safely protects your SLA while unlocking Signals, the faster builder pipeline, and stricter TypeScript that catches bugs earlier.

  • Signals-first mental model

  • Faster builders and stricter TypeScript

  • Stronger a11y and testing expectations

What stakeholders actually ask

“Will the upgrade break our service worker?”, “How do we roll back?”, “What will it do to Lighthouse and INP?” This case study answers those with concrete guardrails, code, and metrics.

Timeline: six weeks to Angular 20 without downtime

Week 1 — Assessment and canary lane

I added Nx around the existing workspace for clearer targets and caching, then stood up a canary route gated by a cookie. We locked baselines with Lighthouse CI, Angular DevTools render counts, and GA4 INP traces.

  • Nx add + project graph cleanup

  • Firebase Hosting cookie canary

  • Bundle/UX baseline recorded

Week 2 — 11→13 and TSLint→ESLint

Why stop at 13 first? It minimized breaking API surface and got us off TSLint. We introduced strictTemplates and fixed a handful of implicit any template errors.

  • ng update @angular/core@13 @angular/cli@13

  • Move to ESLint with recommended rules

  • Fix ViewEncapsulation leakage and zone.js timing issues

Week 3 — 13→15 and RxJS 7 prep

We prepared for RxJS 8 by clearing deprecated patterns. CI codemods caught most of the churn; a few custom operators needed typing fixes.

  • ng update @angular/core@15 @angular/cli@15

  • Replace toPromise() with firstValueFrom/lastValueFrom

  • Remove deep rxjs internals imports

Week 4 — 15→17, builder swap, typed forms opt-in

We moved to the modern builder (faster, better HMR) and brought PrimeNG to the current major with CSS variables. Typed forms were opt‑in only; we left them off to avoid scope creep.

  • ng update @angular/core@17 @angular/cli@17

  • Adopt the new builder pipeline

  • PrimeNG major bump + theme tokens

Week 5 — 17→19→20, TS5 bundler, RxJS 8

The big week: switch to TS5 “bundler”, tighten tsconfig, finish RxJS 8, and stabilize route preloading. Canary ramp-up to 50%.

  • ng update @angular/core@19 then @angular/core@20

  • TypeScript 5 moduleResolution: bundler

  • RxJS 8 API changes and tree-shakable imports

Week 6 — Signals in hot paths + full cutover

We didn’t rewrite state; we used Signals at edges that re-render frequently. After a quiet 48 hours at 50%, we flipped to 100% with a versioned ngsw.json and confirmed no stale assets.

  • Introduce Signals/SignalStore in dashboard filters and totals

  • Service worker cache bust + 100% canary

  • Post-cutover tuning

Breaking changes we actually hit (and how we fixed them)

Example tsconfig updates for TS5 bundler:

{
  "compilerOptions": {
    "target": "ES2022",
    "useDefineForClassFields": true,
    "module": "ES2022",
    "moduleResolution": "bundler",
    "types": ["node"],
    "lib": ["ES2022", "DOM"],
    "paths": {
      "@app/*": ["src/app/*"],
      "@shared/*": ["src/app/shared/*"]
    },
    "strict": true
  },
  "angularCompilerOptions": {
    "strictTemplates": true
  }
}

RxJS 8 replacements that surfaced during CI:

// Before
getData(): Promise<MyDto> {
  return this.http.get<MyDto>(`/api/data`).toPromise();
}

// After
import { firstValueFrom } from 'rxjs';

getData(): Promise<MyDto> {
  return firstValueFrom(this.http.get<MyDto>(`/api/data`));
}

TypeScript 5 bundler resolution

Bundler resolution exposed two circular imports and a path alias that pointed to a barrel with side effects. We split the barrel and pinned moduleResolution to bundler with explicit types and libs.

  • Path aliases must resolve under bundler semantics

  • Barrel cycles surface earlier

RxJS 8 migration

A few services used toPromise and deprecated scheduler imports. We replaced them and leaned on strict types for operator composition.

  • Remove deprecated creation functions

  • Prefer firstValueFrom/lastValueFrom

  • Tree-shakable import paths

PrimeNG theming + templates

p-table had template slot changes and global CSS tokens replaced SASS variables. We moved spacing/typography to tokens to avoid churn later.

  • Move to CSS variables theme

  • Audit p-table template outlets

  • Ripples and icons config changes

Router and SW footguns

We pinned Router options explicitly and versioned the service worker config to break caches on cutover, avoiding the classic 'stuck on old JS' outage.

  • relativeLinkResolution default changes validated

  • Service worker cache busting with version pin

{
  "hosting": {
    "rewrites": [
      { "source": "**", "function": "ssrRouter" }
    ]
  }
}

In the Cloud Function (simplified), route by cookie and serve different build folders:

export const ssrRouter = onRequest(async (req, res) => {
  const isCanary = /x-canary=1/.test(req.headers.cookie ?? "");
  const root = isCanary ? join(process.cwd(), "dist/app-canary") : join(process.cwd(), "dist/app-stable");
  // serve index.html with proper cache headers
});

CI matrix to test both builds every push:

name: ci
on: [push, pull_request]
jobs:
  build-test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        channel: [stable, canary]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npm run build:${{ matrix.channel }} -- --configuration=production
      - run: npm run test:ci
      - run: npm run e2e:ci
      - run: npm run lhci:${{ matrix.channel }}
      - run: npm run a11y:${{ matrix.channel }}

Why cookies

We used a cookie to route traffic to the Angular 20 build without changing DNS. It let support enable the canary for select customers and roll back instantly.

  • Operations can flip specific teams

  • QA can A/B verify easily

Rewrites config

firebase.json snippet for cookie-based routing:

CI matrix

GitHub Actions ran unit, e2e, Lighthouse, and Pa11y across both builds to prevent regressions.

How we used Signals without a rewrite

import { signal, computed } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';

// Stream from WebSocket service
total$ = this.ws.orderTotals$; // Observable<number>

// Bridge to Signals
total = toSignal(this.total$, { initialValue: 0 });

// Local UI state as a Signal
selectedIds = signal<Set<string>>(new Set());

// Derived Signal that feeds a sticky header
selectedCount = computed(() => this.selectedIds().size);

Hot-path adoption

We didn’t replace NgRx everywhere. We introduced Signals and SignalStore only where render thrash hurt: filter chips and totals. RxJS streams stayed for server events; we bridged with toSignal.

  • Dashboard filters

  • Pinned totals header

  • Row‑hover detail card

Bridge pattern

This pattern avoided churn and cut re-renders 40–55% in those components by avoiding over-broadcasted Observables.

Performance outcomes: what moved and by how much

Key deltas (median across 5 top pages)

We verified with repeatable CI (Lighthouse CI on a fixed device profile), real-user GA4, and Angular DevTools in staging. The biggest win came from the builder pipeline, stricter TypeScript, and targeted Signal adoption in high-churn UI.

  • JS transferred: -36% (754 KB → 483 KB)

  • LCP: -31% (3.1s → 2.14s)

  • INP: -27% (238ms → 174ms)

  • Angular DevTools render counts on filter panel: -49%

  • Lighthouse Perf: 83 → 98

Why it improved

TypeScript 5 + modern builder improved tree-shaking and removed legacy polyfills from the Angular 11 era. Pruning legacy operators and moving PrimeNG to tokens reduced both JS and CSS costs.

  • Fewer polyfills and safer dead code elimination

  • No deep RxJS imports

  • PrimeNG CSS variables lowered CSS bloat

When to Hire an Angular Developer for Legacy Rescue

Signals you should bring in help

I’ve done this for a major airline’s kiosk flows (offline tolerant with card readers and printers), a global entertainment company’s employee tracking, and telecom advertising dashboards. If you need a remote Angular developer with Fortune 100 experience, I can own the upgrade end-to-end.

  • You can’t afford downtime (airlines, media ops, payments)

  • Multiple majors behind with TSLint/RxJS 6 still around

  • Service workers or multi-tenant routing make rollouts risky

Typical engagement scope

Discovery within 48 hours, assessment report in a week, and a staged plan with measurable checkpoints. See how gitPlumbers keeps 99.98% uptime during complex modernizations while improving delivery velocity by 70%.

  • 2–4 weeks for rescue/hardening

  • 4–8 weeks for full 11→20 upgrade with CI guardrails

FAQs on upgrading Angular 11 to 20

Related Resources

Key takeaways

  • Six-week, zero-downtime path from Angular 11 to 20 using canary releases and cookie-based routing.
  • Handled high-risk breaks: TS5 moduleResolution bundler, RxJS 8 APIs, ESLint migration, PrimeNG theming changes, SW cache busting.
  • Introduced Signals in hot paths without rewrites; bridged RxJS with toSignal for safe, measurable wins.
  • Performance: -36% JS transferred, -31% LCP, -27% INP, +98 Lighthouse Perf on key views.
  • Guardrails: Nx + GitHub Actions matrix, Lighthouse CI, Pa11y, Cypress, bundle budgets, and feature flags.

Implementation checklist

  • Create a blue/green or cookie-based canary path before touching Angular versions.
  • Upgrade Angular in hops (11→13→15→17→19→20) with ng update and fix per hop.
  • Switch TypeScript moduleResolution to bundler with known-issue allowList.
  • Migrate RxJS to v8: remove deprecated APIs, replace toPromise, prefer scheduled.
  • Move from TSLint to ESLint; enforce strictTemplates and TypeScript strict.
  • Introduce Signals where it pays: table virtualization, filters, header counts.
  • PrimeNG: move to CSS variables theme; audit p-table template slots.
  • Service worker: bust caches with versioned ngsw.json + header-based cache-control.
  • Instrument results: Lighthouse CI, Angular DevTools render counts, GA4/INP traces.
  • Ship with confidence: roll canary to 10% → 50% → 100% under alerting.

Questions we hear from teams

How long does an Angular 11 to 20 upgrade take?
For a mature dashboard with CI, plan 4–8 weeks. I’ve shipped a six‑week path with cookie‑based canaries, weekly hops (11→13→15→17→19→20), and Signals in hot paths. Rescue/hardening only can be 2–4 weeks.
What are the biggest breaking changes to watch?
TypeScript 5 moduleResolution bundler surfacing import issues, RxJS 8 API removals (toPromise, deep imports), PrimeNG template and token shifts, and service worker cache traps. ESLint migration and strictTemplates clean up hidden bugs.
How do you avoid downtime during an upgrade?
Use a canary lane (cookie or header) with instant rollback, keep both builds in CI, run Lighthouse/Pa11y/Cypress across both, and version your service worker to force cache refresh on cutover. Monitor GA4/INP and error rates before ramping traffic.
Do we need to rewrite to Signals/Standalone immediately?
No. Add Signals where it pays (high churn UI). Bridge RxJS streams with toSignal. You can adopt Standalone APIs gradually; NgModules keep working in Angular 20.
How much does it cost to hire an Angular consultant?
It depends on scope and risk. Typical fixed‑fee assessments start at one week; full 11→20 upgrades are usually 4–8 weeks. I’m a senior Angular engineer available for remote contract or consulting. Book a discovery call for a tailored estimate.

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