PERF GUARD: enforce build-time shell and route bundle budgets (#663) #797

Open
opened 2026-10-02 13:10:59 +00:00 by kayg · 0 comments
Owner

PERF GUARD: enforce build-time shell and route bundle budgets (#663)

Finding and context

apps/web/e2e/route-perf.mjs:94 records gzip bytes, but CI only builds the web app.
#497 already records a 321,748-byte shell closure at revision 369ab6a2f against
DESIGN §18's initial-JS <200 kB budget. This is historical evidence, not a new
measurement on current dev. #497 owns size reduction, #164 owns prior route
measurements. This issue adds a deterministic build guard and route coverage.

Exact detection

After the existing production build, parse the Vite client manifest. Root set:
client start/app entries and root layout. Traverse static imports transitively;
count each emitted .js path once. Do not traverse dynamicImports for the shell.
For each registered route, union shell + its layout chain + leaf static import
closure. A dynamic plugin import belongs to its route, not every route. Record
on-demand chunks separately. Verify actual entry/preload links against the
manifest so a preload cannot hide an eagerly loaded chunk. Reject absent
manifest/entry, unresolved import, stale route map or duplicate asset identity.

Gzip each file with fixed level 9 and mtime 0, sum compressed bytes of unique
paths, and compare strict < to budget. The shell budget is 200,000 bytes
(proposed decimal interpretation of 200 kB; record explicitly). Every route has
a numeric JS/CSS budget and on-demand-group budget in the registry. DESIGN
has no per-route CSS or lazy-group numbers: implementation captures one
production baseline and sets initial non-growing ceilings, tied to adoption
issues. Do not invent measured numbers here. A missing ceiling fails. Report
largest chunks, static owner paths and actual/budget/delta on failure.

Existing oversize shell debt needs an explicit #497 exception capped at the
measured implementation-start size with expiry. Do not use the old 321,748
bytes as today's baseline. Route/component renames require map regeneration;
CI never silently blesses new routes or raises ceilings. Generated reports
cannot rewrite the accepted budget file during check. Run after bun run build
in CI; expose a perf-lint bundle subcommand for local gates.

Tests and acceptance

Small checked-in manifest fixtures test static/dynamic edges, shared chunks,
cycles, preload edges, Unicode asset names and missing files. At 199,999 bytes
the shell passes; 200,000 fails. Route-only editor/charts do not charge to the
shell unless statically imported/preloaded. A CSS-only growth and a new route
without a ceiling fail. Two identical build inputs produce identical counts.
No browser or perf VM is needed for byte counts. Keep measured route-open
performance in the existing periodic profile.

Scope and exception contract

Specification only, from #663 perf-guards. Implement in a later job. Reuse the
shared guard registry and ledger described in the coverage issue. Until it
exists, use this exact contract: an exception has rule ID, repo-relative file,
symbol or route/surface ID, normalized syntax hash (or exact build metric),
owner issue, reason, replacement test and UTC expiry date. No directory globs,
line-number-only entries or ignore all comments. Fail on expired, changed,
duplicate and unused exceptions. Existing debt needs an explicit entry and a
non-growing limit. Print exceptions in CI. An exception does not waive access,
session clearing or accessibility tests. Generated and test fixtures are
excluded by explicit source class, not substring matching. Tests must prove
both violation detection and valid exceptions. No product fix belongs here.

The static/contract command exits 1 for violations, 2 for invalid configuration
or parse/build errors, and 0 only for complete passing coverage. Output includes
rule ID, file:line, symbol/route, actual value and required value. No User content.
Measured host timing stays periodic under CLAUDE.md. Deterministic guards fail
required CI; the dedicated perf-budget runner reports measured budget failures
with nonzero status but is not a required merge check.

# PERF GUARD: enforce build-time shell and route bundle budgets (#663) ## Finding and context apps/web/e2e/route-perf.mjs:94 records gzip bytes, but CI only builds the web app. #497 already records a 321,748-byte shell closure at revision 369ab6a2f against DESIGN §18's initial-JS <200 kB budget. This is historical evidence, not a new measurement on current dev. #497 owns size reduction, #164 owns prior route measurements. This issue adds a deterministic build guard and route coverage. ## Exact detection After the existing production build, parse the Vite client manifest. Root set: client start/app entries and root layout. Traverse static imports transitively; count each emitted .js path once. Do not traverse dynamicImports for the shell. For each registered route, union shell + its layout chain + leaf static import closure. A dynamic plugin import belongs to its route, not every route. Record on-demand chunks separately. Verify actual entry/preload links against the manifest so a preload cannot hide an eagerly loaded chunk. Reject absent manifest/entry, unresolved import, stale route map or duplicate asset identity. Gzip each file with fixed level 9 and mtime 0, sum compressed bytes of unique paths, and compare strict `<` to budget. The shell budget is 200,000 bytes (proposed decimal interpretation of 200 kB; record explicitly). Every route has a numeric JS/CSS budget and on-demand-group budget in the registry. DESIGN has no per-route CSS or lazy-group numbers: implementation captures one production baseline and sets initial non-growing ceilings, tied to adoption issues. Do not invent measured numbers here. A missing ceiling fails. Report largest chunks, static owner paths and actual/budget/delta on failure. Existing oversize shell debt needs an explicit #497 exception capped at the measured implementation-start size with expiry. Do not use the old 321,748 bytes as today's baseline. Route/component renames require map regeneration; CI never silently blesses new routes or raises ceilings. Generated reports cannot rewrite the accepted budget file during check. Run after bun run build in CI; expose a perf-lint bundle subcommand for local gates. ## Tests and acceptance Small checked-in manifest fixtures test static/dynamic edges, shared chunks, cycles, preload edges, Unicode asset names and missing files. At 199,999 bytes the shell passes; 200,000 fails. Route-only editor/charts do not charge to the shell unless statically imported/preloaded. A CSS-only growth and a new route without a ceiling fail. Two identical build inputs produce identical counts. No browser or perf VM is needed for byte counts. Keep measured route-open performance in the existing periodic profile. ## Scope and exception contract Specification only, from #663 perf-guards. Implement in a later job. Reuse the shared guard registry and ledger described in the coverage issue. Until it exists, use this exact contract: an exception has rule ID, repo-relative file, symbol or route/surface ID, normalized syntax hash (or exact build metric), owner issue, reason, replacement test and UTC expiry date. No directory globs, line-number-only entries or `ignore all` comments. Fail on expired, changed, duplicate and unused exceptions. Existing debt needs an explicit entry and a non-growing limit. Print exceptions in CI. An exception does not waive access, session clearing or accessibility tests. Generated and test fixtures are excluded by explicit source class, not substring matching. Tests must prove both violation detection and valid exceptions. No product fix belongs here. The static/contract command exits 1 for violations, 2 for invalid configuration or parse/build errors, and 0 only for complete passing coverage. Output includes rule ID, file:line, symbol/route, actual value and required value. No User content. Measured host timing stays periodic under CLAUDE.md. Deterministic guards fail required CI; the dedicated perf-budget runner reports measured budget failures with nonzero status but is not a required merge check.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#797
No description provided.