Files
govoplan-core/docs/WEBUI_BUNDLE_BUDGETS.md
T

87 lines
3.9 KiB
Markdown

# WebUI Loading And Bundle Budgets
The Core WebUI host owns the loading boundary for installed module packages.
Vite discovers configured packages at build time, but emits an asynchronous
loader for each package's `src/module.ts` contribution descriptor. At runtime,
Core imports only descriptors whose backend manifests are enabled and identify
the matching `frontend.package_name`.
The direct descriptor entry is intentional. A package root may re-export pages
for consumers; importing that barrel as module wiring can cause those pages to
be evaluated before navigation. Route pages and substantial panels should use
`React.lazy`, and Core wraps routes in the shared loading/error boundary.
## Enforced Budgets
`webui/bundle-budget.json` contains the production limits:
| Measurement | Raw limit | Gzip limit |
| --- | ---: | ---: |
| Initial JavaScript static import closure | 512 KiB | 160 KiB |
| Largest individual asynchronous JavaScript chunk | 384 KiB | 110 KiB |
`npm run build` writes a Vite manifest, measures the entry and its recursive
static imports, writes `dist/bundle-metrics.json`, and fails when either budget
is exceeded. `npm run test:module-permutations` applies the same gate to every
permutation and records the collected results in
`dist/module-permutation-bundle-metrics.json`. In CI, each result is also added
to the step summary.
Budgets are limits, not targets. A change that approaches a limit should add a
new lazy boundary or remove unnecessary entry code instead of raising the
limit without measurement and review.
## 2026-07-30 Baseline
Measurements use the same full-product source tree and Node 22 runtime. The
post-change build additionally includes the Search module in the default and
full-product sets.
| Initial-load measurement | Before | After | Reduction |
| --- | ---: | ---: | ---: |
| JavaScript assets in initial static closure | 1 | 1 | 0% |
| Raw JavaScript | 1,387,043 B | 453,769 B | 67.3% |
| Gzip level 9 | 364,767 B | 141,725 B | 61.1% |
| Brotli quality 11 | 254,797 B | 106,701 B | 58.1% |
| Parse proxy median | 18.776 ms | 7.750 ms | 58.7% |
| Parse proxy p95 | 21.980 ms | 8.739 ms | 60.2% |
The parse proxy constructs a fresh `node:vm` `SourceTextModule` from the entry
source 30 times with a randomized source marker. It is useful for a controlled
before/after comparison, but is not enforced in CI because absolute timings
vary across runners. Transfer budgets use deterministic raw and gzip byte
counts.
The first budgeted full-product build reported:
- initial JavaScript: 453,769 B raw / 141,725 B gzip;
- largest async chunk: `CampaignWorkspace`, 353,724 B raw / 98,142 B gzip.
## Verification
Development startup explicitly prebundles the Excel reader's browser/universal
entrypoints and the lazy rich-text editor's Tiptap dependencies. These are
Core-installed vendor dependencies, not eager optional-module imports. This
avoids first-time Campaign/Template navigation triggering a second dependency
optimization and page reload. Module descriptors and pages remain lazy, and
production bundle budgets remain unchanged. The Core interface-pattern check
verifies this include list and keeps optional GovOPlaN modules excluded.
```bash
cd /mnt/DATA/git/govoplan-core/webui
npm run build
npm run check:bundle-budget
npm run test:module-permutations
```
The build gate also catches accidental eager imports: a page pulled into the
entry closure consumes the initial budget, while an oversized page or module
descriptor consumes the asynchronous chunk budget.
The startup shell imports appearance validation/application from the pure
`appearanceOverrides.ts` runtime. Settings-only color controls, JSON import/export,
and previews remain in `AppearanceOverridesEditor.tsx` behind the existing lazy
Settings route. Importing a runtime helper from a module that also owns editor
components can accidentally pull the entire editor into the startup chunk.
Public helper exports remain compatible; theme application is still synchronous.