From dee1d04a70ace07cd1aad3fc481222f836417f5f Mon Sep 17 00:00:00 2001 From: 4gray Date: Tue, 13 Oct 2026 09:38:26 +0200 Subject: [PATCH] perf(web): lower the initial-bytes baseline to 1,345,810 and document the startup-path rules The five previous commits took Angular Material off the renderer's startup path: renderer.initialBytes went from 1,596,201 to 1,344,067 bytes (-252,134, 15.8%) on a local production build at the branch point, measured with `pnpm nx build web --skip-nx-cache --output-path=dist/measure-web --stats-json` after each commit. Rebased onto master (#1905 adds 1,743 bytes of backup code to the initial path), the branch measures 1,345,810, and the baseline follows that; the ratchet and direction checks pass against it. evidencePr is filled in once the pull request exists. The performance contract explains the chunk-level rule behind the work, and the UI guidelines gain a "Material off the startup path" section with the form-field, dialog, deferred panel, theme scope and animations rules and the guards that keep them. The release note covers the user-visible effect. Co-Authored-By: Claude Fable 5.1 --- .changes/web-material-off-startup-path.md | 9 +++++ docs/architecture/iptvnator-ui-guidelines.md | 41 ++++++++++++++++++++ docs/architecture/performance-journeys.md | 18 +++++++++ tools/performance/journey-baselines.json | 6 +-- 4 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 .changes/web-material-off-startup-path.md diff --git a/.changes/web-material-off-startup-path.md b/.changes/web-material-off-startup-path.md new file mode 100644 index 000000000..ab30daa24 --- /dev/null +++ b/.changes/web-material-off-startup-path.md @@ -0,0 +1,9 @@ +--- +type: perf +area: web +--- + +The app loads about 15% less code before it shows its first screen: form +field, dialog, tooltip and progress-bar code, the animation engine and the +theme colours of unused components now load only when something needs them, +so every launch has less to download and parse. diff --git a/docs/architecture/iptvnator-ui-guidelines.md b/docs/architecture/iptvnator-ui-guidelines.md index 5886a24ae..9f07e1045 100644 --- a/docs/architecture/iptvnator-ui-guidelines.md +++ b/docs/architecture/iptvnator-ui-guidelines.md @@ -88,6 +88,47 @@ tokens, so the retired `--mdc-*` names compile but do nothing; a spec loads as raw CSS cannot use Sass modules; it declares the `--mat-*` token directly and says why. +### Material off the startup path + +`renderer.initialBytes` ([performance journeys](performance-journeys.md#rendererinitialbytes)) +counts every module an eager file imports, and the bundler keeps a module in +one chunk, so one import of one export carries the whole module. Material +therefore reaches the initial bundle only through the snack-bar, which root +services inject (and with it `button`, the icon button and the CDK overlay). +Everything else is lazy by these rules: + +- **Form-field defaults.** Import `AppFormFieldModule` + (`@iptvnator/ui/components/form-field`) instead of `MatFormFieldModule` + wherever a template renders ``: it exports Material's form + field with a directive that provides the app defaults (outlined, dynamic + subscript) on the element. The defaults are not a root provider, and a + route-level one would miss dialogs opened by root services. + `pnpm run styles:form-field-defaults:validate` (CI) rejects a component that + renders a form field without the module. +- **Dialogs from services created with the app.** A root service that opens a + dialog resolves `MatDialog` through `LazyMatDialog` + (`@iptvnator/ui/components/lazy-dialog`) and imports the dialog component + on demand, as `DialogService`, the player service, the parental lock prompt + and the workspace shell actions do; `openConfirmDialog` returns a promise + that settles once the dialog is open. A component in a lazy chunk injects + `MatDialog` directly. +- **Panels the shell mounts on every launch.** The EPG progress panel and the + update notification are an eager host with `@defer (when visible)` around + the view that imports Material; the startup status draws its spinner as + static markup copied from `mat-spinner`. A new always-mounted component + follows the same split. +- **Theme scope.** `m3-theme.scss` themes only the components in its + `$material-components` list, through `meta.apply` for both the light theme + and the dark colours. Add an entry with a new `@angular/material/` + import and remove it with the last one; + `pnpm run styles:material-theme-scope:validate` (CI) checks both directions + (`core` always stays, `button` brings `icon-button` and `fab`). +- **No animations module.** Nothing declares an Angular animation trigger and + Material 22 animates with CSS, deciding from `prefers-reduced-motion` itself, + so `app.config.ts` does not call `provideAnimations()`; the Electron + `material-motion.e2e.ts` checks that dialogs, selects, menus, snack-bars and + tooltips still animate in and out and stop under reduced motion. + The app theme is the `.dark-theme` class that `SettingsService` puts on ``; a "System" choice resolves the OS preference there and nowhere else. Component styles never use a `prefers-color-scheme` query, which diff --git a/docs/architecture/performance-journeys.md b/docs/architecture/performance-journeys.md index fac0b08be..8095a7df0 100644 --- a/docs/architecture/performance-journeys.md +++ b/docs/architecture/performance-journeys.md @@ -1156,6 +1156,24 @@ every build, which only the `development`, `electron-e2e` and `performance-build-config.spec.ts` keeps it that way. Removing the static import lowered the counter by 12,565 bytes. +The bundler keeps each module in one chunk, so one eager import of any +export carries the module's whole code: providing +`MAT_FORM_FIELD_DEFAULT_OPTIONS` in `app.config.ts` alone put the 54 KB +form-field chunk on the initial path, although every form field renders in a +lazy route or dialog. Angular Material therefore stays off the startup path by +construction, with the rules and guards in the +[UI guidelines](iptvnator-ui-guidelines.md#material-off-the-startup-path): +form-field defaults travel with `AppFormFieldModule`, services created with +the app resolve `MatDialog` through `LazyMatDialog`, the panels the shell +mounts on every launch defer their Material views, the theme emits tokens only +for imported components, and the app does not provide the animations module, +which nothing used (Material 22 animates with CSS). Together these lowered the +counter from 1,596,201 to 1,344,067 bytes (-252,134, 15.8%) on a local +production build at the branch point; the baseline follows the measurement +of the branch rebased onto `master`, 1,345,810. The snack-bar stays: root +services inject it, and with it Material's button, the icon button chunk and +the CDK overlay (about 190 KB) remain initial. + `--summary` writes the journey summary shape (`journeys..counters`) that the ratchet checker consumes. `--dist ` points the script at another build output, for example the `electron-performance` configuration. diff --git a/tools/performance/journey-baselines.json b/tools/performance/journey-baselines.json index ee8841485..b82fba3d0 100644 --- a/tools/performance/journey-baselines.json +++ b/tools/performance/journey-baselines.json @@ -3,11 +3,11 @@ "journeys": { "launch": { "renderer.initialBytes": { - "value": 1595777, + "value": 1345810, "unit": "bytes", "slack": 4096, - "updatedAt": "2026-10-07", - "evidencePr": 1810, + "updatedAt": "2026-10-13", + "evidencePr": null, "measuredWith": "pnpm nx build web && pnpm run perf:initial-bytes" }, "renderer.ipcCallsToFirstCard": {