# 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.