Skip to content

perf(angular): fix CLS regression from critical-CSS inlining + drop rxjs from the bundle - #34

Merged
lissy93 merged 5 commits into
lissy93:mainfrom
nigrosimone:perf-angular-v2
Aug 9, 2026
Merged

perf(angular): fix CLS regression from critical-CSS inlining + drop rxjs from the bundle#34
lissy93 merged 5 commits into
lissy93:mainfrom
nigrosimone:perf-angular-v2

Conversation

@nigrosimone

Copy link
Copy Markdown
Contributor

Summary

Two independent optimisations to the Angular app:

  1. Fix a CLS regression caused by Angular's critical-CSS inlining worth +23 Lighthouse performance points.
  2. Remove rxjs from the shipped bundle by moving rxResource to resource().

1. CLS: disable inlineCritical

Angular's critical-CSS inlining (beasties) runs against the built index.html, where
<app-root> is still empty. It therefore only finds :root / html / body rules to
inline, and defers the real stylesheet:

<link rel="stylesheet" href="styles-*.css" media="print" onload="this.media='all'">

The app paints unstyled, then the stylesheet arrives and re-lays out the whole page.
Lighthouse attributes a 0.724 layout shift to <main class="main">.

Since nothing renders before the JS bootstrap anyway, making the stylesheet render-blocking
costs nothing on FCP/LCP and removes the shift entirely.

Metric before after
Performance 77 100
CLS 0.724 0
FCP 0.4 s 0.4 s
LCP 0.5 s 0.5 s
TBT 10 ms 0–10 ms
Speed Index 0.4 s 0.4 s

Accessibility and best-practices stay at 100.

2. Drop rxjs from the bundle

resource() is stable public API in Angular 22 (@publicApi 22.0) and takes a promise
loader, so @angular/core/rxjs-interop and the rxjs operators are no longer reachable:

  • weather-state.service.ts: rxResource({ stream }) to resource({ loader })
  • weather.service.ts: Observables to async/await over the existing fetch calls

rxjs stays in package.json it is still a required peer dependency of @angular/core but its operators no longer end up in the output.

before after
main JS (raw) 168.17 kB 158.64 kB
main JS (transfer) 48.65 kB 45.86 kB
Initial total (raw) 185.57 kB 176.04 kB
Initial total (transfer) 52.03 kB 49.24 kB

Bonus: the loader now forwards resource()'s abortSignal down to fetch, so switching
city actually cancels the in-flight request instead of letting it race.

eslint.config.mjs gains AbortSignal: 'readonly' alongside the other browser globals.

nigrosimone and others added 5 commits August 9, 2026 06:19
Angular was the only app of 15 that pulled the four shared design-system
stylesheets through angular.json's `styles` array, emitting them as a
bundled styles-<hash>.css. Every other framework (react, vue, svelte,
preact, solid, lit, qwik, astro, alpine, jquery, vanilla, vanjs, lume-js,
geajs) links them from index.html as /styles/*.css.

That inconsistency had two effects:

  - the benchmark's bundle-size analysis excludes shared assets by name
    and by directory, so the other 14 apps never get these bytes counted
    while Angular did -- 17.4 kB raw / 3.79 kB gzipped of pure penalty
  - Angular served one minified bundle where everyone else serves four
    unminified files, so the Lighthouse comparison was not like-for-like
    either

Aligning Angular with the other 14 makes both measurements comparable.

Counted bundle (same method as scripts/benchmark/bundle_size.py):

  before   176,043 raw / 55,224 gzipped
  after    158,642 raw / 51,430 gzipped
  react    158,538 raw / 50,256 gzipped   (published 2025-09-09)

Lighthouse is unchanged at performance 100 / a11y 100 / best-practices 100,
CLS 0, verified over two runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lissy93
lissy93 merged commit f864dcf into lissy93:main Aug 9, 2026
30 checks passed
@nigrosimone
nigrosimone deleted the perf-angular-v2 branch August 9, 2026 09:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants