
Angular 20 Upgrade Playbook: Surviving Angular CLI, TypeScript 5+, and RxJS 8 Breaking Changes Without Downtime
A zero‑drama, zero‑downtime plan to move enterprise Angular apps to 20+—taming CLI builder shifts, TypeScript 5.x strictness, and RxJS 7→8 changes with CI guardrails.
Upgrades don’t break production—untested assumptions do. Treat Angular 20 as a delivery project with canaries, budgets, and typed backoffs, and you’ll sleep fine.Back to all posts
The night before a release at a telecom, our real‑time dashboard started jittering. The culprit wasn’t Highcharts. It was an eager RxJS upgrade that changed retry semantics and flooded our WebSocket reconnects. I’ve since standardized a zero‑downtime path for Angular 20+ upgrades—CLI, TypeScript, and RxJS included—so your dashboards don’t blink and your PM doesn’t call you at midnight.
As enterprises plan 2025 Angular roadmaps and Angular 21 betas land, here’s the playbook I use on Fortune 100 systems—airport kiosks (Docker‑simulated hardware), employee tracking at a global entertainment company, and ad analytics at a major telecom—using Nx, Signals/SignalStore where appropriate, PrimeNG, and Firebase or Azure pipelines.
Upgrade commands I actually run on client codebases (run in hops and commit between steps):
# lock Node and PNPM/NPM via .nvmrc and .npmrc or pnpm-workspace.yaml
node -v && npm -v
# snapshot deps
npm pkg get dependencies devDependencies > deps.snapshot.json
# Angular hops (example path; adjust from your current major)
ng update @angular/core@15 @angular/cli@15 --force
ng update @angular/core@16 @angular/cli@16 --force
ng update @angular/core@17 @angular/cli@17 --force
ng update @angular/core@18 @angular/cli@18 --force
ng update @angular/core@20 @angular/cli@20 --force
# RxJS if not auto-migrated
ng update rxjs@^7 --force # then address 8 with codemods when readyThe Upgrade Scene: No PagerDuty Alerts
Why I’m strict about upgrades
I’ve upgraded Angular apps in production windows with live airline kiosks and telecom dashboards streaming millions of events. The only way we survived: branch, canary, and measure. Everything below turns that into a repeatable system you can hand to your team—or bring me in to run as your Angular consultant.
Dashboards must not jitter during peak traffic.
CI should prove safety before any production exposure.
Monorepos need incremental, testable hops.
Why Angular CLI, TypeScript, and RxJS Break During Major Upgrades
CLI build/test shifts
Angular’s new builder stack is faster but stricter. Misplaced asset globs, legacy budgets, or custom webpack hooks will fail hard when you move to esbuild/Vite. Treat this as a migration, not just a version bump.
Vite/esbuild build system since Angular 17.
Builder options moved/renamed; polyfills handling changed.
Karma on life support; teams migrate to Jest/Vitest.
TypeScript 5.x strictness
The Angular CLI expects modern TS settings. If you keep Node/TS misaligned, you’ll get mysterious import and class field errors—especially in SSR and libs shared across workspaces.
moduleResolution: bundler
useDefineForClassFields defaults changed.
Decorator type emit changes since TS 5.
RxJS 7→8 semantics
Telemetry and WebSocket flows behave differently under RxJS 8 if you copy legacy retryWhen patterns. Without typed wrappers and backoff limits, reconnect storms can take down dashboards.
toPromise removed; use firstValueFrom/lastValueFrom.
retryWhen patterns replaced by retry options.
Operator import paths tightened.
A Zero‑Downtime Upgrade Plan: Branch, Canary, Measure
# .github/workflows/upgrade-ci.yml
name: upgrade-ci
on: [push]
jobs:
build_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20.x', cache: 'npm' }
- run: npm ci
- run: npx nx run-many -t lint,test --parallel=3
- run: npx nx build web --configuration=production
- run: npx lhci autorun || echo "LH warnings allowed in canary"
preview:
needs: build_test
runs-on: ubuntu-latest
steps:
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: ${{ secrets.GITHUB_TOKEN }}
firebaseServiceAccount: ${{ secrets.FIREBASE_SA }}
channelId: canary
projectId: your-firebase-projectBranch and lock
Work in a long‑lived branch with dedicated CI. Use feature flags to disable non‑critical realtime and SSR during canaries if needed.
Create upgrade/
branch. Freeze dependency drift with lockfiles.
Enable feature flags for risky surfaces (SSR, WebSockets).
Dual builds + canaries
Firebase previews are the fastest way I’ve seen to stand up safe canaries across environments. Azure/GCP/AWS blue‑green works too—just keep promotion scripted.
Build old and new in CI; compare budgets.
Ship to Firebase Hosting preview channels or a blue‑green slot.
Send 5–10% traffic to the new build first.
Monitor and promote
Promote only when error budgets are green. On IntegrityLens we target 0.02 CLS and held that through Angular 20; on gitPlumbers we maintained 99.98% uptime through three major upgrades.
Error rate delta < 1%.
Core Web Vitals no regression (LCP/INP/CLS).
No increase in reconnects/timeouts.
Angular CLI Breaking Changes to Handle Before You Flip the Switch
// angular.json (excerpt)
{
"projects": {
"web": {
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser-esbuild",
"options": {
"outputPath": "dist/web",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": ["zone.js"],
"assets": ["src/assets"],
"budgets": [
{ "type": "initial", "maximumError": "180kb" },
{ "type": "anyComponentStyle", "maximumError": "6kb" },
{ "type": "bundle", "name": "main", "maximumError": "2300kb" }
]
}
}
}
}
}
}Move to the new builders
The CLI will migrate most settings, but custom webpack merges or exotic assets need a manual pass. Keep budgets aggressive to catch accidental regressions.
Replace legacy browser builder with esbuild.
Audit assets, budgets, and polyfills.
SSR/hydration paths need a smoke test.
Budgets that stop bad deploys
Budgets are your last line before a regression hits prod. I set stricter budgets in canary and relax slightly for production if needed, but never remove them.
bundleSize: error at 2.3MB (example).
initial LCP bundles < 180KB gz.
enforce CSS budget for PrimeNG themes.
TypeScript 5.x Settings That Quiet the Red Squiggles
// tsconfig.json (excerpt)
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM"],
"moduleResolution": "bundler",
"useDefineForClassFields": true,
"strict": true,
"noImplicitOverride": true,
"skipLibCheck": false,
"types": ["node", "jest"],
"paths": {
"@app/*": ["apps/web/src/*"],
"@shared/*": ["libs/shared/src/*"]
}
}
}Align with Vite/esbuild
This prevents import edge cases and class field surprises. It also keeps SSR consistent across Node and browser builds.
moduleResolution: bundler
target: ES2022, lib: [DOM, ES2022]
useDefineForClassFields: true
Raise the floor—not the pain
I enable strict in steps: start with app code, then libs, then enable in tsconfig.base for the whole workspace. Measure red/green via CI to avoid team thrash.
strict: true and noImplicitOverride.
skipLibCheck: false for libs you own.
paths for Nx libs only; rootDir careful with SSR.
RxJS 7→8 Migrations That Won’t Wake PagerDuty
import { Observable, firstValueFrom, retry, timer, TimeoutError, timeout } from 'rxjs';
export function backoff(maxRetries = 5, initialMs = 500) {
return retry({
count: maxRetries,
delay: (_err, retryCount) => timer(initialMs * Math.pow(2, retryCount - 1) + Math.random() * 100)
});
}
export async function fetchJson<T>(obs$: Observable<T>, abortMs = 8000): Promise<T> {
try {
return await firstValueFrom(obs$.pipe(timeout({ each: abortMs })));
} catch (e) {
if (e instanceof TimeoutError) {
// log to Firebase/GA4 here
}
throw e;
}
}Replace toPromise and friends
The old toPromise masked hangs. Make the replacement explicit and time‑bounded so upstream issues don’t freeze views.
Use firstValueFrom/lastValueFrom.
Wrap in timeout to prevent hangs.
Use AbortController for fetch flows.
Stop reconnect storms
Typed reconnects stopped our telecom charts from blinking. This pattern has shipped safely in multiple real‑time apps.
Use retry with count+delay instead of retryWhen.
Exponential backoff with jitter.
Circuit‑break after N failures.
Prefer typed wrappers
Wrap raw RxJS in utility operators so mistakes don’t leak through dozens of call sites.
Centralize HTTP/WebSocket operators.
Emit discriminated unions for errors.
Log to GA4/Sentry/Firebase on terminal errors.
Example From the Field: Telecom Analytics and Airline Kiosks
# docker-compose.kiosk.yml (excerpt)
services:
kiosk:
build: .
environment:
- MOCK_SCANNER=1
- MOCK_PRINTER=1
ports:
- "4200:4200"
scanner:
image: mock/scanner:latest
printer:
image: mock/printer:latestTelecom analytics upgrade
We upgraded a real‑time dashboard with typed telemetry events and SignalStore‑backed charts. WebSocket reconnect storms dropped to near‑zero; Core Web Vitals stayed green during canaries.
Angular 16→20 with Vite/esbuild.
RxJS backoff + typed events.
-12% main bundle, -32% build time.
Airport kiosk hardening
Barcode scanners, printers, card readers—simulated with Docker so we could reproduce bugs fast. The upgrade held offline‑tolerant flows and device state transitions without regressions.
Docker‑simulated peripherals in CI.
TS 5 strict on device adapters.
Zero downtime cutover during low‑traffic window.
When to Hire an Angular Developer for Legacy Rescue
Bring in help when…
If your team is juggling features and an upgrade, you need a dedicated lane. I stabilize codebases, add Nx guardrails, and deliver the upgrade without halting delivery. See how we keep velocity at 70%+ on gitPlumbers while modernizing.
AngularJS/9–14 code + rushed deadlines.
Custom webpack builders or SSR edge cases.
Real‑time streams, kiosks, or multi‑tenant RBAC.
What to Change Next After the Upgrade
Signals and SignalStore where it counts
I don’t rewrite everything. I introduce Signals/SignalStore for view state and keep RxJS for telemetry streams—best of both worlds.
Replace flaky RxJS state with Signals where local.
Keep streaming lanes RxJS; bridge with toSignal where needed.
Polish and prove
Once the platform is stable, we invest in UX polish that moves metrics—fewer regressions, better INP, and happier users.
Chromatic/Storybook visual guardrails.
AA accessibility audits.
SSR hydration smoke + Lighthouse.
Key takeaways
- Treat upgrades as delivery work, not a side quest—branch, canary, and monitor or you’ll page people at 2 a.m.
- Move to the Angular 17+ build system (Vite/esbuild) intentionally; audit builders, budgets, and polyfills before flipping.
- Lock TypeScript to a known‑good 5.x version and set moduleResolution: bundler to align with Vite/esbuild.
- Use codemods for RxJS 7→8 (firstValueFrom/lastValueFrom, retry options), then add typed wrappers so failures are visible.
- Prove safety with CI: dual builds (old/new), E2E smoke, Lighthouse, and Firebase preview channels for canary traffic.
Implementation checklist
- Create an upgrade branch and enable canary deploys (Firebase Hosting previews or blue‑green).
- Run ng update in hops (e.g., 12→15→16→17→18→20) or Nx migrate with a lockfile snapshot.
- Migrate to the new build system; validate budgets, assets, and SSR/hydration paths.
- Pin TypeScript 5.x, set moduleResolution: bundler, target ES2022, enable useDefineForClassFields.
- Run RxJS codemods: replace toPromise, migrate retryWhen, remove deprecated result selectors.
- Add GitHub Actions jobs: unit, E2E, SSR smoke, Lighthouse, and bundle-size checks.
- Gate releases behind feature flags; measure error rates, LCP/INP, and WebSocket reconnect health.
- Roll out in 5–10% canaries; promote only when error budgets are green.
Questions we hear from teams
- How long does an Angular upgrade to 20+ take?
- Typical engagements run 4–8 weeks for full upgrades across CLI, TypeScript 5+, and RxJS 7→8 with CI guardrails. Emergency rescues or smaller apps can ship in 2–4 weeks with canary deploys and feature flagging.
- What does an Angular consultant actually do during an upgrade?
- I audit the build and test stack, run ng update in safe hops, migrate to Vite/esbuild, align TypeScript 5 settings, codemod RxJS, add budgets, and set up canary deploys with monitoring so production never blinks.
- How much does it cost to hire an Angular developer for this work?
- Scope drives cost. Fixed‑price assessments start at a few thousand; full upgrade projects are typically time‑and‑materials. I offer an initial codebase review and plan, then phase the work to match budget and risk.
- Do we have to migrate to Signals during the upgrade?
- No. We can ship a safe platform upgrade first. I often add Signals/SignalStore incrementally for local UI state while keeping RxJS for real‑time streams and data services.
- Can we keep Karma or do we need Jest/Vitest?
- You can keep Karma short‑term, but most teams move to Jest or Vitest for speed and ecosystem support. I migrate tests as a follow‑on once the platform is stable.
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