From 155a5aed63a093516c1c5551798ed09d8acd6277 Mon Sep 17 00:00:00 2001 From: leojay Date: Sun, 30 Aug 2026 18:45:04 +0100 Subject: [PATCH] chore(ops): per-route bundle budgets (#77) Split the single shared-JS size-limit into per-group budgets (framework/runtime, marketing routes, docs, explore) with limits set just above the measured next@15 baseline, so a PR that pulls in a heavy dependency fails the existing `npm run size` CI step in review. Adds docs/bundle-budget.md with baselines, enforcement, how to run a local analyzer pass, and the planned dynamic imports for the wallet SDK and charting. The @next/bundle-analyzer dev dependency is deferred to a follow-up (needs its lockfile regenerated on the CI Node version). Refs #77 --- .size-limit.json | 33 ++++++++++++++++++++++ docs/bundle-budget.md | 64 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 docs/bundle-budget.md diff --git a/.size-limit.json b/.size-limit.json index 76fdbb0..8cd08a2 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -4,5 +4,38 @@ "path": [".next/static/chunks/*.js"], "limit": "300 KB", "gzip": true + }, + { + "name": "Framework + shared runtime (gzip)", + "path": [ + ".next/static/chunks/framework-*.js", + ".next/static/chunks/main-*.js", + ".next/static/chunks/webpack-*.js" + ], + "limit": "115 KB", + "gzip": true + }, + { + "name": "Marketing route chunks (gzip)", + "path": [ + ".next/static/chunks/app/page-*.js", + ".next/static/chunks/app/product/**/*.js", + ".next/static/chunks/app/roadmap/**/*.js", + ".next/static/chunks/app/contributors/**/*.js" + ], + "limit": "8 KB", + "gzip": true + }, + { + "name": "Docs route chunks (gzip)", + "path": [".next/static/chunks/app/docs/**/*.js"], + "limit": "10 KB", + "gzip": true + }, + { + "name": "Explore route chunks (gzip)", + "path": [".next/static/chunks/app/explore/**/*.js"], + "limit": "12 KB", + "gzip": true } ] diff --git a/docs/bundle-budget.md b/docs/bundle-budget.md new file mode 100644 index 0000000..555d866 --- /dev/null +++ b/docs/bundle-budget.md @@ -0,0 +1,64 @@ +# Bundle size budget + +The site is mostly static marketing today, but the wallet SDK, charting, and +the dashboard are heavy additions. Without a budget that growth is invisible +until the site is slow. This is the budget and how it is enforced. + +## Enforcement + +CI runs `npm run size` (`size-limit`) against the build it just produced, in +the same job as lint/typecheck/build (`.github/workflows/ci.yml`). A pull +request that pushes any tracked group over its limit fails the check, so a +heavy dependency is caught in review rather than after release. + +## Baselines + +Measured with `next@15`, gzipped, on the current `main`: + +| Group | Budget | Baseline | Headroom | +| --- | --- | --- | --- | +| Shared First Load JS | 300 KB | 254.2 KB | ~46 KB | +| Framework + shared runtime | 115 KB | 98.6 KB | ~16 KB | +| Marketing route chunks (`/`, `/product`, `/roadmap`, `/contributors`) | 8 KB | 0.6 KB | large | +| Docs route chunks (`/docs/**`) | 10 KB | 2.3 KB | large | +| Explore route chunk (`/explore`) | 12 KB | 1.9 KB | large | + +Route budgets are deliberately tight — marketing routes are what most +visitors see and they should ship almost no client JavaScript. Raise a +budget only with a comment explaining what was added and why it is worth it. + +## Analyzing a regression + +When `npm run size` flags a group, find the dependency that moved it with a +one-off local analyzer run — kept out of `package.json` so CI installs stay +lean: + +```bash +npm i -D @next/bundle-analyzer # local only, do not commit +``` + +then wrap the export in `next.config.ts`: + +```ts +import withBundleAnalyzer from "@next/bundle-analyzer"; +export default withBundleAnalyzer({ enabled: true })(nextConfig); +``` + +and `npm run build` writes per-route treemaps to `.next/analyze/`. Revert +both once you have the answer. + +## Planned follow-up + +Tracked to keep this budget green as features land: + +- **Analyzer in CI** — add `@next/bundle-analyzer` as a dev dependency and an + `ANALYZE=true` build artifact once the lockfile is regenerated on the CI + Node version, so per-route treemaps are available on every PR. +- **Wallet SDK** — `@stellar/freighter-api` / `components/wallet/*` is + currently pulled into the shared bundle via the header. Move it behind + `next/dynamic` (`ssr: false`) so it only loads on interaction / routes + that connect, then add a `Wallet SDK` budget entry pointing at the split + chunk. +- **Charting** — `components/charts/*` should load only on the dashboard, + behind `next/dynamic`. +- Keep marketing routes as Server Components with minimal `"use client"`.