Zero‑Downtime Angular 11 → 20 Upgrade: Real Timeline, Breaking Changes, and Proven Performance Gains

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@^17

Week 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 functions

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

Hire Matthew – Remote Angular Expert (Available Now) See how I rescue chaotic Angular code

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