Zero‑Downtime Angular Upgrade: 11 → 20 in 6 Weeks — RxJS 8, TypeScript 5, Vite, and a 41% Bundle Cut

Zero‑Downtime Angular Upgrade: 11 → 20 in 6 Weeks — RxJS 8, TypeScript 5, Vite, and a 41% Bundle Cut

A real upgrade story from a leading telecom analytics dashboard: step‑by‑step timeline, breaking changes handled, and measurable performance wins—without taking production down.

“Six weeks. Zero downtime. 41% smaller bundles and a 58% faster build pipeline—while the dashboard stayed live through peak sports traffic.”
Back to all posts

I’ve done a lot of Angular upgrades for Fortune 100 teams. The one below—an 11→20 jump for a leading telecom’s advertising analytics dashboard—was the cleanest: six weeks, zero downtime, and a 41% bundle reduction. Here’s the exact timeline, the breaking changes we tackled, and the performance we measured.

The Night the Graphs Stuttered: Why We Upgraded From Angular 11 to 20 Without Downtime

Challenge

On a Thursday night during a playoff game, the charts on a telecom advertising dashboard jittered under peak load. I’d built similar real‑time dashboards for a broadcast media network and an airline ops center—I knew the fix wasn’t another debounce. It was time to modernize: Angular 11 to 20, but production couldn’t blink.

  • Spiky traffic during live sports drove CPU to 85%

  • Bundle bloat and long cold starts from Webpack

  • RxJS and TypeScript drift blocking new features

Constraints

We had to maintain enterprise SSO, audit trails, and multi-tenant isolation. The UI leaned on PrimeNG components with custom tokens—breaking those would create costly retraining issues. We chose a parallel upgrade track with canary traffic and aggressive observability.

  • Zero downtime, enterprise SSO, and strict audit logs

  • PrimeNG-heavy UI with custom theming

  • Multi-tenant roles and ABAC permissions

Project Context: Advertising Analytics at Telecom Scale

What we were upgrading

The app ingested billions of events/day, summarized spend and reach across campaigns, and streamed WebSocket deltas to PrimeNG tables/charts. The codebase was stable but dated. CI ran on GitHub Actions; hosting and previews were on Firebase Hosting with Cloud Functions for APIs.

  • Angular 11, Nx 12, RxJS 6.x

  • Webpack build, TSLint, TS 4.1

  • PrimeNG 11, custom theme, D3/Highcharts mix

Goals we set with leadership

Stakeholders care about reliability and speed. We made performance budgets visible in CI and agreed on a sunset for dead code once tracking proved no regressions.

  • Zero downtime and steady conversion rates

  • <2s p95 LCP on core dashboards

  • Cut build times by 50% and reduce bundle >30%

Timeline: Six Weeks, Zero Downtime

Week 1 — Audit and safety rails

We started with a dependency diff and set up a parallel CI lane that built both the current (prod) and the upgrade branch. A Firebase canary channel received 5% of traffic from internal users. Cypress smoke ran on both lanes.

  • Dependency inventory and compatibility map

  • Two-lane CI: current vs next

  • Firebase canary channel with 5% traffic

Week 2 — 11→12→13 and ESLint migration

We upgraded one major at a time, merging fixes only when tests passed. ESLint caught a few implicit anys and zone-unaware tests.

  • ng update across majors with tests green each step

  • TSLint→ESLint using @angular-eslint schematics

  • Fix strict templates and any types

Week 3 — 14→15→16 and esbuild

Build times dropped immediately with esbuild. We added a theme shim so PrimeNG tokens wouldn’t break visual consistency.

  • Switch to esbuild builder (v16) for faster builds

  • PrimeNG to 15 with token shims

  • RxJS 7 codemods, begin deprecating toPromise

Week 4 — 17 and Vite builder

Vite improved HMR for the team and shaved ~30% off prod bundles. TS 5 required moduleResolution: bundler and path alias fixes.

  • Migrate to new Angular builder (Vite under the hood)

  • Update TypeScript to 5.x; fix moduleResolution

  • Rebaseline budgets and source maps

Week 5 — 18→19→20 and RxJS 8

We closed the loop on RxJS 8 and re-tuned a few retry strategies for WebSocket reconnects used in the live dashboards.

  • RxJS 8 finalization (firstValueFrom, tap import paths)

  • Audit deprecated APIs, enable optional inject()

  • Re-run e2e with network throttling

Week 6 — Hardening and promotion

We scaled canary traffic while watching GA4, Firebase logs, error budgets, and Angular DevTools render counts. With clean signals, we promoted to 100% without a blip.

  • Load tests, Lighthouse/INP sweeps, a11y pass

  • Promote canary to 25%→50%→100%

  • Post-cutover watch with automatic rollback

Breaking Changes We Navigated from Angular 11 to 20

// before (Angular 11 / RxJS 6)
const data = await http.get<Report>(url).toPromise();

// after (Angular 20 / RxJS 8)
import { firstValueFrom, retry, map } from 'rxjs';

const data = await firstValueFrom(
  http.get<Report>(url).pipe(
    retry({ count: 3 }),
    map(r => ({ ...r, loadedAt: Date.now() }))
  )
);

# Stepwise majors with tests at each hop
npx nx run-many -t test --all
ng update @angular/core@12 @angular/cli@12 --force
ng update @angular/core@13 @angular/cli@13 --force
# ... iterate up to 20
ng update @angular/core@20 @angular/cli@20 --force

Angular CLI → esbuild → Vite

We moved from Webpack to esbuild in v16, then to Vite in v17+. Both required revisiting budgets and source maps, and validating lazy-loaded routes.

  • 16: esbuild builder

  • 17+: Vite builder with faster prod bundles

TypeScript 5.x

TS 5 forced us to clean up a few legacy types. Result: clearer contracts for chart adapters and websocket services.

  • moduleResolution: bundler

  • Decorator metadata changes surfaced hidden any

RxJS 8

We applied codemods and audited async/await usage in effects and resolvers. Example:

  • toPromise removed; use firstValueFrom/lastValueFrom

  • Operator imports updated

Implementation Walkthrough: Branch Strategy, CI, and Safe Releases

# .github/workflows/upgrade-canary.yml
name: Upgrade Canary
on:
  push:
    branches: [upgrade/angular20]
jobs:
  build-test-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: corepack enable && pnpm i --frozen-lockfile
      - run: pnpm nx affected -t lint test build --parallel=3
      - run: pnpm nx run web:build --configuration=production
      - name: Deploy canary
        run: pnpm firebase deploy --only hosting:canary --project telecom-analytics

// angular.json (snippets)
{
  "projects": {
    "web": {
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser-esbuild", // v16
          "options": { "budgets": [{"type":"initial","maximumWarning":"1.8MB","maximumError":"2.2MB"}] }
        }
      }
    }
  }
}
// After Angular 17+
{
  "projects": {
    "web": {
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser", // Vite under the hood
          "options": { "budgets": [{"type":"initial","maximumWarning":"1.4MB","maximumError":"1.8MB"}] }
        }
      }
    }
  }
}

Two-lane CI and canaries

The canary channel ran behind SSO so only internal traffic hit it. We wired GA4 to segment users by build ID and watched Core Web Vitals before promotion.

  • Build/test current and upgrade branches in parallel

  • Deploy upgrade to Firebase canary channel

Angular builders and budgets

We updated the builder progressively and re-baselined budgets to reflect Vite’s chunking.

  • angular.json: esbuild then Vite

  • Budgets to catch regressions

ESLint and quality gates

Static analysis prevented a lot of churn during rapid major bumps.

  • @angular-eslint, strict templates, codelyzer rules replaced

  • Pre-commit + CI lint

UI and Theming: PrimeNG Changes Without Retraining

/* theme-shim.scss */
:root {
  --brand-primary: #2b6cb0; // old $brand-primary
  --surface-1: #0f172a;
}
.p-button.p-button-primary { background: var(--brand-primary); }
.p-datatable .p-datatable-thead > tr > th { background: var(--surface-1); }

PrimeNG token shims

We preserved look-and-feel while moving to modern PrimeNG. We also added Storybook snapshots to catch theming drift.

  • Mapped legacy SCSS vars → CSS vars

  • Kept density/spacing identical

A11y polish ride-along

Small accessibility wins came “for free” while we touched components, something I push on every engagement.

  • Color contrast AA, focus rings, table semantics

Performance Gains: Before vs After

import { signal, computed } from '@angular/core';

export class FilterStore {
  private readonly _region = signal<string | null>(null);
  private readonly _campaign = signal<string | null>(null);
  readonly params = computed(() => ({
    region: this._region(),
    campaign: this._campaign(),
  }));
  setRegion = (v: string | null) => this._region.set(v);
  setCampaign = (v: string | null) => this._campaign.set(v);
}

What we measured

We treat performance like a feature. Results below are from the same dashboards under realistic traffic.

  • Angular DevTools render counts on hot tables

  • Lighthouse, p95 LCP/INP, error budgets

  • Bundle sizes and build times

Results

Signals/SignalStore came in after the version upgrade for the busiest filter panel. Here’s a simplified pattern we used:

  • 41% main bundle reduction (2.4MB → 1.41MB)

  • Build times down 58% (11m → 4m 36s)

  • p95 LCP 3.2s → 1.9s; p95 INP 220ms → 110ms

  • Render counts -34% after introducing Signals in filters

How an Angular Consultant Approaches Signals Migration Post-Upgrade

Why post-upgrade

We avoid coupling framework upgrades with state rewrites. After v20 was stable, we migrated filters and data-intensive widgets to Signals/SignalStore.

  • Reduce risk: separate versioning from state refactors

  • Target hot paths first

Guardrails

We sample render counts and use feature flags to compare RxJS-based selectors vs signal-based computed values. Promotion happens only when metrics improve.

  • Angular DevTools flame charts and render counts

  • Feature flags around store usage

When to Hire an Angular Developer for Legacy Rescue

Common triggers

If you need a senior Angular engineer who has upgraded Fortune 100 apps under load, bring someone in early. As an Angular consultant, I pair with your team, keep delivery moving, and leave you with guardrails you’ll keep using.

  • Budget pressure but growing performance debts

  • Blocked by RxJS/TS/CLI drift

  • Production incidents tied to build tooling

Takeaways and Next Steps

Key lessons

This upgrade finished in six weeks without downtime, cut the main bundle by 41%, halved build times, and improved Core Web Vitals. If you’re planning an Angular 20+ roadmap and want a steady hand, I’m available as a remote Angular contractor.

  • Do majors one at a time with tests green at every hop.

  • Use canary channels and telemetry; promote when clean.

  • Move to esbuild/Vite early to unlock big wins.

  • De-risk state refactors; adopt Signals after the upgrade.

  • Measure everything; publish the wins.

Related Resources

Key takeaways

  • Upgrade majors incrementally (11→12→…→20) behind canaries and traffic splitting to avoid downtime.
  • Tackle breaking changes in isolation: RxJS 8 (toPromise→firstValueFrom), TypeScript 5 moduleResolution, ESLint migration, PrimeNG theming.
  • Move from Webpack to esbuild (16) then Vite (17+) for faster builds and smaller bundles.
  • Introduce Signals/SignalStore post-upgrade on hot paths to reduce renders before fully going zoneless.
  • Instrument everything: DevTools render counts, Lighthouse, Core Web Vitals, GA4/Firebase logs, and error budgets.

Implementation checklist

  • Inventory dependencies and lock versions by major (Nx, Angular CLI, PrimeNG, RxJS).
  • Create an upgrade branch with CI matrices (current vs next) and canary deploys.
  • Run ng update per major; fix tests before proceeding.
  • Migrate TSLint→ESLint, RxJS 7→8 codemods, TS 5 config changes.
  • Switch to esbuild (v16) then Vite builder (v17+) with budgets and source maps verified.
  • Smoke test critical flows (auth, tables, exports) with Cypress on every canary.
  • Roll out feature slices behind route-level gates; promote when telemetry is clean.

Questions we hear from teams

How long does an Angular 11 to 20 upgrade take?
For a mature app with solid tests, expect 4–8 weeks. This case took six weeks with stepwise majors (11→12→…→20), parallel CI lanes, and canary releases. Smaller apps can move faster; complex monorepos can take longer.
What does zero downtime mean for an Angular upgrade?
Users never see a maintenance page. We ship to a canary channel, route a small traffic slice, watch telemetry (LCP/INP, errors), then gradually promote to 100%. Automatic rollback is ready if budgets breach.
What are the biggest breaking changes from Angular 11 to 20?
RxJS 8 (toPromise removal, import path updates), TypeScript 5 moduleResolution changes, builder shifts to esbuild/Vite, and TSLint→ESLint. UI libraries like PrimeNG/Material may require theme and component API updates.
Do we need to adopt Signals during the upgrade?
Not required. I typically decouple framework upgrades from state refactors. Stabilize on v20 first, then migrate hot paths to Signals/SignalStore to capture render and UX wins with less risk.
How much does it cost to hire an Angular developer for an upgrade?
It varies by scope and test coverage. Typical engagements range from a 2–4 week assessment to a 4–8 week full upgrade. I offer fixed-fee assessments and weekly rates for delivery. Discovery call within 48 hours.

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 we rescue chaotic Angular apps at 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