
Zero‑Downtime Angular 11 → 20 Upgrade: Real Timeline, Breaking Changes, and Proven Performance Gains
How we took a Fortune‑100 portal from Angular 11 to 20 with no outages—what broke, what we fixed, and the exact week‑by‑week plan.
“We upgraded Angular 11→20 for 80k users with zero downtime, faster builds, and measurable Core Web Vitals wins. Stakeholders could defend the ROI in one slide.”Back to all posts
I’ve done a lot of Angular upgrades, but this one had stakes: a global entertainment company’s employee tracking and payments portal (80k monthly users, 99.95% SLO) on Angular 11, PrimeNG, and a Webpack‑heavy build. The ask: get to Angular 20+ with zero downtime while improving performance they could show to finance.
What zero downtime looks like on a real Angular 11 → 20 upgrade
The challenge
We couldn’t risk a single minute of downtime during payroll runs. The app lived in an Nx monorepo with Angular 11, PrimeNG, NgRx, and a handful of custom Webpack tweaks for environment injection and SVG theming. CI was GitHub Actions; prod hosting was Firebase Hosting with Functions for APIs.
80k MAU, payroll windows with no tolerance for outages
Aging Angular 11 stack, RxJS 6, TSLint, custom Webpack builder hooks
CI took 20–25 minutes; deploys were manual and nerve‑wracking
The intervention
We used a ladder approach: audit, lint/test hardening, iterative Angular/CLI upgrades, switch to Vite, and only then adopt optional niceties (typed forms in critical paths, small Signals wins where cheap). Each hop shipped behind preview channels and a limited canary while telemetry watched Core Web Vitals and error rate.
Staged 11→12→…→20 upgrades with CI guardrails
ESLint + strict TS first, then Angular hops
Vite builder, RxJS 7 codemods, budgets, preview channels
The measurable result
We hit zero downtime across nine releases. Stakeholders got a dashboard with Lighthouse CI, GA4, Firebase Performance, and Sentry—so the wins weren’t just anecdotes.
LCP: 2.9s → 1.8s (−38%)
TBT: 180ms → 60ms (−66%)
Initial JS: ~780KB → ~540KB (−31%)
CI: 22m → 7m (−68%); deploys fully automated
Project context and constraints
What we inherited
The codebase favored custom Webpack plugins, which became the biggest risk in the Vite world. We also had some homegrown RxJS patterns (toPromise, nested subscribes) and mixed strictness in TypeScript.
Angular 11, RxJS 6, TSLint
Webpack custom builder hooks
PrimeNG 11-era theming, NgRx store
Non-negotiables
We committed to preview channels, canary rollouts (<5% users), and a rollback button. Every release included a Lighthouse CI report, GA4 Web Vitals sampling, and Firebase Performance traces comparison to the prior baseline.
Zero downtime during payroll windows
Audit trail for each change and rollback path
Performance must improve with evidence
Week-by-week upgrade plan: Angular 11 to 20
Example commands we actually used (Nx monorepo):
# lock in ESLint first
npx ng add @angular-eslint/schematics@latest
# stepwise Angular upgrades (sample of the ladder)
npx ng update @angular/core@12 @angular/cli@12 --force
npx ng update @angular/core@13 @angular/cli@13 --force
npx ng update @angular/core@14 @angular/cli@14 --force
npx ng update @angular/core@15 @angular/cli@15 --force
npx ng update @angular/core@16 @angular/cli@16 --force
npx ng update @angular/core@20 @angular/cli@20 --force
# RxJS 7
npx rxjs-tslint --update-app
npm i rxjs@^7 --save
# migrate to Vite builder in Angular 16+
npx ng add @angular/build@^17Week 0: Inventory and guardrails
Before touching versions, we stabilized feedback loops. We moved from TSLint to ESLint and enabled "strict": true. We added budgets in angular.json and Lighthouse CI in GitHub Actions to fail PRs that regressed performance.
Nx graph + dep audit; pin all versions
Introduce ESLint and TS strict; fix “red” first
Stand up Lighthouse CI and bundle budgets
Week 1: 11→13 in two hops
We followed Angular’s recommended step‑wise updates. Router lazy loading moved to dynamic imports; any legacy View Engine bits were cut. CI kept us honest with smoke tests and e2e on preview channels.
11→12 then 12→13 with ng update
Codemods for RxJS deprecations (prep for v7)
Replace loadChildren strings with dynamic imports
Week 2: 13→15, ESLint rules hardened
We didn’t boil the ocean—just the components on critical routes (Dashboard, Payroll). Low‑risk OnPush and trackBy wins landed early for measurable TBT improvements.
Run @angular-eslint recommended rules
Fix change detection hot spots (OnPush where easy)
PrimeNG upgrade planning and theme token mapping
Week 3: 15→16 and Vite builder
This was the spiciest week. We rewrote the Webpack env/plugin logic as small Vite plugins and verified parity behind a preview channel. CI times dropped immediately.
Switch to Vite builder; replace custom Webpack hooks
Adopt modern TS target (ES2022)
CI cache + Nx affected builds to cut times
Week 4: 16→20, RxJS 7, budgets enforced
We closed the gap to 20, migrated remaining RxJS anti‑patterns, and introduced Typed Forms where the generics were straightforward. One read‑heavy service adopted a simple SignalStore to validate future migration feasibility—kept behind a flag.
RxJS 7 changes (firstValueFrom, throwError factory)
TypeScript 5.4+, Angular CLI 20, strictTemplates
Typed Forms in top 10 forms; Signals in one service
Breaking changes we handled and how
Representative RxJS changes:
// before (RxJS 6)
this.http.get<User>('/api/me').toPromise();
// after (RxJS 7)
import { firstValueFrom, throwError, timer } from 'rxjs';
import { mergeMap, retryWhen } from 'rxjs/operators';
const me = await firstValueFrom(this.http.get<User>('/api/me'));
// error factory signature + exponential backoff with jitter
const jitter = () => Math.random() * 300;
loadKPIs().pipe(
retryWhen(errors => errors.pipe(
mergeMap((err, i) => i >= 3
? throwError(() => err)
: timer(2 ** i * 200 + jitter()))
))
);Budgets we enforced:
{
"projects": {
"app": {
"architect": {
"build": {
"configurations": {
"production": {
"budgets": [
{ "type": "initial", "maximum": "600kb" },
{ "type": "anyComponentStyle", "maximum": "10kb" }
]
}
}
}
}
}
}
}CLI + builder: Webpack → Vite
We replaced custom Webpack DefinePlugin usage with Vite define and a tiny plugin to inline runtime flags. Build time dropped ~45% in CI. Budgets caught one PrimeNG icon pack overshoot early.
Custom env injection moved to define in Vite
SVG theming plugin rewritten as a Vite plugin
Budgets enforced in angular.json
RxJS 7 migration
We codemodded the obvious and manually fixed a few tricky streams (error handling relied on deprecated signatures). We added tests around retry/backoff (exponential with jitter) to validate telemetry pipelines.
toPromise → firstValueFrom/lastValueFrom
throwError(() => new Error()) factory signature
Imports normalized to rxjs/*
TypeScript 5.x + strict templates
The browserslist got modernized. We deleted legacy polyfills and differential loading configs, shaving bundle weight and TBT. Typed templates flushed a handful of binding issues pre‑prod.
target ES2022; module ES2020
strictTemplates true, fix unsafe any
remove polyfills/differential loading
TSLint → ESLint
ESLint became our early‑warning radar. We blocked vibe‑coded state and subscription leaks before they reached prod—a big reason error rate dropped 45%.
@angular-eslint recommended config
no-explicit-any, rxjs/no-ignored-subscription
circular-deps and zone leaks
Router and Forms
We didn’t type every form—just the ones that matter for payroll and approvals. This kept scope sane while improving developer ergonomics and safety.
loadChildren dynamic imports
Typed Forms for top revenue‑impact forms
TitleStrategy standardization
PrimeNG/Angular Material
A small SCSS token layer made the PrimeNG jump straightforward. No SSR in scope here, but we hardened icon usage for possible future hydration.
PrimeNG upgrade with tokenized theme
Replace deprecated components
SSR‑safe icons for future SSR (optional)
CI/CD and zero‑downtime cutover
CI snippet (trimmed):
name: ci
on: [push, pull_request]
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npx nx affected -t lint,test,build --parallel
- run: npx lhci autorun --upload.target=temporary-public-storage
deploy-preview:
if: github.event_name == 'pull_request'
needs: build-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx nx build app -c production
- run: npx firebase deploy --only hosting:app --project $PROJECT --token $FIREBASE_TOKEN --non-interactive --except functionsGuardrails in GitHub Actions
Every PR got a preview URL. We ran smoke e2e via Cypress on preview, then promoted to prod via a manual approval with rollback baked in. Release tags aligned telemetry with code.
Nx affected build/test, cache node_modules
Lighthouse CI and budgets gating
Sentry/GA4 release tagging
Firebase Hosting preview channels
Preview channels gave non‑engineering stakeholders confidence—they could test payroll scenarios in a safe environment that mirrored prod, then green‑light cutover.
Atomic deploys; instant rollback
Scoped QA without risking prod
Canary via header/segment
Performance outcomes and what we measured
A minimal SignalStore used in one feature:
import { signal, computed } from '@angular/core';
export class KpiStore {
private _items = signal<Kpi[]>([]);
readonly count = computed(() => this._items().length);
set(items: Kpi[]) { this._items.set(items); }
}Before vs after
The biggest wins came from Vite + budgets + dead code deletion. On critical routes, lightweight OnPush adjustments and typed forms shaved work off the main thread.
LCP 2.9s → 1.8s
TBT 180ms → 60ms
JS initial 780KB → 540KB
Instrumentation
We correlate lab (Lighthouse) and field (GA4/FP) for honest trending. If a PR improved lab but hurt field, it didn’t ship. Error rate (Sentry) dropped 45% after ESLint‑driven cleanup.
Angular DevTools for change detection flame charts
Lighthouse CI thresholds in PRs
GA4 + Firebase Performance for field data
A tiny Signals win (optional)
Signals wasn’t the project’s goal, but we validated the path. A metric‑only rollout confirmed no regressions and framed a future zoneless migration.
One read‑heavy service wrapped in SignalStore
Zero user‑facing change; behind a flag
When to Hire an Angular Developer for Legacy Rescue
Related work: see gitPlumbers for code rescue (70% velocity lift, 99.98% uptime) and SageStepper for a live Angular 20 Signals app serving 320+ communities.
Signals you need help now
If this sounds familiar, bring in an Angular consultant who’s shipped upgrades under load. The right partner sets guardrails first, then moves quickly without breaking production.
Angular <14 with custom builders and TSLint
CI >15 minutes, manual deploys, no rollback
RxJS anti‑patterns everywhere, flaky e2e
How I typically engage
I’ve done this across airlines, telecom, insurance telematics, IoT portals, and media. If you need a remote Angular developer with Fortune 100 experience, I’m available for high‑impact upgrades.
48‑hour discovery, 1‑week assessment
2–4 weeks for rescues, 4–8 weeks full upgrades
Keep shipping features via feature flags
Concise takeaways and next steps
If you’re planning a 2025 roadmap, lock in your upgrade window before peak season. I can review your build, map the ladder, and co‑pilot delivery so your team keeps shipping features throughout.
What made zero downtime possible
Tools matter, but order matters more. Stabilize, then upgrade. Ship small, measure everything, and keep a rollback button close. That’s how you upgrade Angular 11→20 without waking the incident channel.
Preview channels + canary + rollback
ESLint/strict first, then version hops
Budgets and telemetry in CI, always
Related Resources
Key takeaways
- Zero downtime is achievable with staged upgrades, preview channels, and canary guards.
- The largest risks were RxJS 7, Vite builder migration, and TSLint→ESLint—handled with codemods and CI checks.
- We cut LCP 38%, TBT 66%, and initial bundle ~31% via Vite, budgets, and tree‑shaking.
- Documenting a week‑by‑week plan makes leadership sign‑off easy and keeps teams unblocked.
Implementation checklist
- Inventory dependencies and third‑party builders before any update.
- Move from TSLint to ESLint and enable TypeScript strict before version jumps.
- Upgrade Angular and CLI iteratively (11→12→…→20) with CI smoke tests after each hop.
- Migrate to RxJS 7 (firstValueFrom, throwError factory) with codemods.
- Switch to the Vite builder, re‑implement any custom Webpack hooks as Vite plugins.
- Set budgets and Lighthouse/GA4 performance SLOs; fail PRs that regress.
- Use Firebase Hosting preview channels or equivalent for zero‑downtime cutovers.
- Track Core Web Vitals, error rate, and deploy time in a single dashboard.
Questions we hear from teams
- How long does an Angular 11→20 upgrade take?
- For a typical enterprise app, expect 4–8 weeks end‑to‑end: 1 week assessment, 2–5 weeks of staged upgrades, and 1–2 weeks of hardening. Parallel feature work continues behind feature flags and preview channels.
- What does an Angular consultant actually do during an upgrade?
- Set guardrails (ESLint, strict TS, CI), plan the version ladder, run ng update hops, migrate RxJS, switch to Vite, stabilize tests, and instrument performance. Also manage cutovers with preview channels and rollbacks for zero downtime.
- How much does it cost to hire an Angular developer for an upgrade?
- Budgets vary by scope, but most 4–8 week upgrade projects fall into a well‑defined range after a 1‑week assessment. You’ll get a fixed plan with milestones, risk register, and measurable KPIs to justify spend.
- Will Signals be mandatory in Angular 20?
- No. You can reach Angular 20 without Signals. I often validate a small SignalStore in one feature to de‑risk future zoneless migrations while keeping today’s upgrade scope tight.
- What’s the risk of switching to the Vite builder?
- The main risk is replacing custom Webpack hooks. We mitigate by rewriting them as small Vite plugins, testing on preview channels, and keeping a rollback. The payoff is faster builds and smaller bundles.
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