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 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 committed 2026-10-13 09:51:58 +02:00
1 parent 85c5a078fb
commit dee1d04a70
4 files changed
+71 -3

No files matched your search

@@ -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.
@@ -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 `<mat-form-field>`: 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/<entry>`
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
`<body>`; a "System" choice resolves the OS preference there and nowhere
else. Component styles never use a `prefers-color-scheme` query, which
+18
View File
@@ -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.<journey>.counters`)
that the ratchet checker consumes. `--dist <dir>` points the script at another
build output, for example the `electron-performance` configuration.
+3 -3
View File
@@ -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": {