Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/docs/architecture/bundle-budgets.md
T

7.1 KiB

Bundle Budgets

Status: active Owner: web platform Related: scripts/bundle-budgets.json, scripts/verify-bundle-size.mjs

Why

Web bundle growth is silent unless we measure it. The boilerplate tracks two complementary signals:

  1. Group budgets (BUDGETS constant in verify-bundle-size.mjs): coarse caps for root / initial_payload_js / vendor / route / other / lazy_runtime / application_js / total_js / css / manifest_js. Hard CI gate — failure exits 1.
  2. Per-route, per-vendor and per-lazy-runtime budgets (scripts/bundle-budgets.json): fine-grained gzip limits for routes, vendor chunks and self-hosted feature runtimes/workers. Non-blocking — surfaces as a PR-comment with a Δ-from-main column. Reviewers decide whether growth is justified.

The per-route layer was added in task #71 (Phase 10) to make day-to-day creep visible without blocking legitimate feature work.

initial_payload_js recursively follows the static imports of the React Router entry and root modules. Dynamic imports stay outside that boot signal.

Self-hosted feature runtimes need a separate signal. lazy_runtime contains Monaco, PDF.js and emitted Web Worker entry points: these bytes replaced executable CDN downloads and are loaded only when the feature or worker is activated. application_js preserves the pre-self-hosting cap for the app itself, while total_js still caps every emitted JavaScript byte including lazy runtimes. This keeps CDN removal from weakening either critical-path or overall artifact controls.

Config schema

scripts/bundle-budgets.json validated by scripts/bundle-budgets.schema.json:

{
  "default": { "limitKb": 200, "warningKb": 180 },
  "routes": {
    "app/$tenant/dashboard": { "limitKb": 250, "warningKb": 225 },
    "auth/magic-link": { "limitKb": 80, "warningKb": 75 },
  },
  "vendorChunks": {
    "vendor-react": { "limitKb": 150, "warningKb": 135 },
  },
  "lazyRuntimeChunks": {
    "MonacoEditor.client": { "limitKb": 1000, "warningKb": 950 },
    "ts.worker": { "limitKb": 1550, "warningKb": 1520 },
  },
}
  • All sizes are gzip kilobytes (the transport-relevant number).
  • limitKb is the hard cap; assets above this report [EXCEED] in CI output and the PR comment.
  • warningKb is the soft threshold; between warningKb and limitKb assets report [WARN].
  • Routes use React Router v7 route ids in either dot- or slash-form (auth/magic-link and auth.magic-link both match).
  • Unmatched routes fall back to default.
  • Unmatched vendor chunks (anything starting with vendor- or chunk-) are reported under the chunk basename and also fall back to default.
  • Monaco/PDF.js chunks and *.worker-* entry points use lazyRuntimeChunks; these budgets make formerly hidden CDN payloads reviewable without treating them as route modules.

Local workflow

# Build apps/web normally and print the per-route table:
pnpm --filter @hivegrid/web build
node scripts/verify-bundle-size.mjs

# Only the per-route table (skips group checks):
node scripts/verify-bundle-size.mjs --per-route-only

# Machine-readable JSON (used by the PR-comment workflow):
node scripts/verify-bundle-size.mjs --json > pr-bundle-report.json

--json emits only the non-blocking per-route/per-vendor report and exits with code 0 when the report was produced. Keep the hard group-budget gate on the plain node scripts/verify-bundle-size.mjs path so redirected JSON never mixes machine-readable output with human diagnostics.

Visualizer (treemap)

To inspect why a chunk is big:

pnpm --filter @hivegrid/web run bundle:visualize
open apps/web/build/stats.html

The visualizer is gated on ANALYZE_BUNDLE=true so the regular production build keeps its current footprint. It uses rollup-plugin-visualizer (treemap, gzip + brotli sizes per chunk).

Security note: stats.html contains source-map references and should never be deployed publicly. Treat it as a developer-only artifact.

Gitea workflow

.gitea/workflows/bundle-budget.yml runs on every relevant Gitea pull request. The dedicated workflow keeps the expensive web build outside the default CI lane while still enforcing the aggregate budget.

The workflow:

  1. Build @hivegrid/web.
  2. Run verify-bundle-size.mjs --json.
  3. Download the latest bundle-baseline-main artifact (captured by bundle-budget-baseline.yml on main pushes).
  4. Render a markdown report via scripts/render-bundle-budget-comment.mjs.
  5. Upload the JSON and Markdown reports as Gitea Actions artifacts.

The comment renders rows in severity order ([EXCEED] → [WARN] → unbudgeted → ok), with columns: status / kind / route or chunk key / current gzip size / configured limit / Δ from main. Δ is — if no baseline is available.

When the baseline is missing

The first PR after a fresh checkout (or after a baseline-artifact expiry) sees Δ from main = — for every row. This is expected — the next push to main re-captures the baseline.

When to raise a limit

  1. Confirm the growth is intentional (a new feature shipped, a dependency landed) — not a regression from accidental imports.
  2. Open the pull request; the bundle-budget artifact shows the Δ. Reviewers and the author should agree the new floor is acceptable.
  3. Edit scripts/bundle-budgets.json in the same PR that ships the growth. Bump warningKb to ≈ (current gzip + 5 KB) and limitKb to ≈ (current gzip + 20 KB).
  4. Call the change out in the PR description so the new floor is reviewable.

Anti-patterns

  • Don't disable a budget by raising it to 9999 KB. The point is to surface drift. If a route is legitimately huge, raise the limit to a tight new floor, not infinity.
  • Don't lazy-load critical-path code. Bundle splits help non-critical routes, but the recursively measured entry/root static graph still has its own initial_payload_js hard group budget that must stay tight.
  • Don't bundle-split shared deps. Splitting react into many chunks just shifts cost into chunk boot-up. Keep vendor-react, vendor-core-utils etc. as single chunks; the manualChunks mapping in apps/web/app/utils/build/manualChunks.ts is the source of truth.

Troubleshooting

  • Report artifact fehlt: check the Bundle Budget workflow run — the Build web step may have failed (e.g. server-only import in a client export).
  • A worker appears as a route: worker filenames must match *.worker-* (or PDF.js's *.worker.min-*) so the verifier puts them in lazy_runtime; update the narrow matcher when adding a differently named worker entry.
  • CI run is much slower than pnpm typecheck: the workflow does a full pnpm --filter @hivegrid/web build. That's the expected cost. If it ever exceeds ~30 minutes, profile with ANALYZE_BUNDLE=true locally first.

Rollback

  • If the per-route budget surfaces too many false alarms, lower the warning thresholds in scripts/bundle-budgets.json and ship a follow-up.
  • If the entire PR-comment flow is broken, set if: false on the measure job in bundle-budget.yml — no other CI stage depends on it.