Skip to content

web: set and enforce a precache bundle budget for the MUI migration #825

Description

@mforce

Component plan: build this from the components named in docs/designs/822-mui-revamp.md. The rule is "reused, never rebuilt" — composing MUI primitives into a screen wrapper is expected; reimplementing behaviour a shared component already has is not. Say in the PR which row of that table covers this screen.

Part of #674. Groundwork.

Measured on 2026-09-13 (docs/decisions/674-ui-component-library.md):

precache JS gzip
baseline, hand-rolled 1312.45 KiB 85.27
MUI provider only, zero components 1397.79 KiB 115.45
MUI + a realistic component kit 1632.68 KiB 186.98

+320 KiB precache (+24%) for a full kit. This is a PWA for phones in sheds.

Scope

  • Set an explicit precache ceiling.
  • Enforce it in CI so it fails a PR rather than being discovered at screen 13.
  • Decide how a slice that adds weight without retiring hand-built code is justified.

Runtime cost is a separate question and currently looks fine: at 6x CPU throttling the converted
Dashboard rendered in 1168 ms median vs 1213 ms hand-rolled — inside the noise. Re-measure when a
screen uses Autocomplete or a data grid.

Activity

  1. added
    sliceThin vertical work item
    epic-674SPA revamp — field-first phone, ledger desktop (#674)
    on Sep 13, 2026
  2. added this to the SPA revamp milestone on Sep 13, 2026
  3. mforce commented on Sep 14, 2026

    @mforce
    OwnerAuthor

    Runtime baseline measured (2026-09-14) — the slope, not the value, is what this slice must watch

    Three production builds, 9 runs each, 6x CPU throttling, phone viewport (390x844), timed to the
    last Dashboard panel visible. Static build served locally with /api proxied to a dev API, so the
    server and seed data are identical across all three and only the client differs.

    build wall script style recalc layout precache
    base, no MUI 917 ms 107 ms 15 ms 26 ms 1312.45 KiB
    + theme provider only 921 ms 119 ms 15 ms 26 ms 1397.79 KiB
    + MUI Dashboard (11 components) 919 ms 136 ms 15 ms 25 ms 1436.90 KiB

    What this settles

    • Wall time is flat — 4 ms spread across 27 runs. ~900 ms is network and API, so MUI is
      invisible end to end at this scale.
    • Style recalculation does not move: 15 ms in all three. This retires the concern that
      Emotion's runtime CSS injection would be expensive on low-end phones. It is not.

    What this slice should therefore budget

    Script time is the cost that scales: +12 ms for the provider alone, +29 ms with eleven
    components, at 6x throttle — about +5 ms on the device. That is +27% script time for a light
    dose of MUI.

    So a bundle-only budget is the wrong instrument on its own. Recommend this slice sets two
    ceilings:

    1. Precache bytes — the number this slice already names.
    2. Script duration on a reference screen, because it is what actually grows per component and a
      byte budget cannot see it.

    Re-measure when a screen adopts Autocomplete or a data grid (#826 is the first): the slope above
    is measured on Paper/Typography/Chip/Divider only, and must not be assumed flat.

    Method is reproducible — CDP Performance.getMetrics deltas around each navigation, median of 9.
    Full numbers in docs/decisions/674-ui-component-library.md.

  4. mforce commented on Sep 16, 2026

    @mforce
    OwnerAuthor

    Numbers for the ceiling choice (2026-09-16, head f502222)

    Measured on a throwaway worktree with live imports (an upper bound, since some of these components may already be in the bundle after #829/#830):

    build precache
    head 1501.43 KiB
    + MUI Dialog family, Autocomplete, TextField, Tooltip, IconButton 1631.76 KiB (+130.3)
    hand-built Dialog + useConfirm + NamedEntityPicker, isolated as one chunk (what #826/#827 retire) 37.5 KB raw JS
    hand-built NumberField, isolated (what #828 retires) 18.2 KB raw JS

    Projection after #826, #827 and #828 (option (a)): about 1,590 KiB before their CSS is deleted. #835's opsz adds 118.9 on top, so about 1,710. A 1,600 ceiling goes red at #835 and is marginal at #827. 1,800 leaves roughly 90 KiB for the seven remaining screens.

    Not yet chosen. The owner asked what the ceiling is for and whether it applies to one control or the whole shell; answered in session, recorded here once decided.

  5. mforce commented on Sep 16, 2026

    @mforce
    OwnerAuthor

    Decision (owner, 2026-09-16): measure and report, no gate

    The precache total is printed in CI on every web PR (extend web/scripts/verify-sw.mjs, which already extracts the manifest, to sum the listed files from dist/ and print the KiB), with no threshold. Same shape as backend coverage (#776): a floor picked before enough slices have been measured is a guess, and a wrong guard reads as safety. Revisit a ceiling once the control slices (#826, #827, #828) and #835 have landed and the number has a trend.

    What this changes in the design doc: D9 ("Ceiling 1,800 KiB, enforced in CI") and the §7 owner-review row are amended when this slice lands. The "a slice that adds more than 10 KiB without deleting code names the reason in its PR body" rule stays as a convention. The script-duration ceiling from the comment above is likewise report-only for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:frontendReact/Vite web clientepic-674SPA revamp — field-first phone, ledger desktop (#674)phase:1-groundworkWhole-app decisions before any screen convertspriority:tier2High value, low risksliceThin vertical work item

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions