ci: bundle-size budget — tree-shaken min+gzip, CI-gated#85
Conversation
Measures the honest 'lean install' number a bundler ships for a real app (minified + gzipped), per representative tree-shaken entry point, and fails CI if a build grows past a committed budget (with 3% headroom; ratchet down with `pnpm size:update` when a build shrinks). Measured 2026-06-08 (min+gzip): kernel (world + systems + scheduler) ~40 KB, @ecsia/core alone ~30 KB, full umbrella ~55 KB. ecsia is batteries-included so it's larger than a minimal core like bitECS (~5 KB); the packages are sideEffects:false, so unused subsystems (relations, serialization, topics) drop. The budget guards against silent inflation, not toward a 5 KB target (a different design). Adds `pnpm size` / `pnpm size:update`, a CI step in build-test, and an honest bundle-size note on the performance page.
Greptile SummaryThis PR adds a tree-shaken min+gzip bundle-size gate: a new
Confidence Score: 4/5The CI gate works correctly and pnpm size will catch genuine bundle growth; the main gap is that pnpm size:update can silently absorb regressions. The new size-check script and CI step are functionally sound. The one workflow gap is that --update ignores the failed flag and exits 0 even when it writes larger-than-before budget values, so a careless pnpm size:update during a regression erases the gate without any additional log noise. The CI matrix redundancy is a minor inefficiency with no correctness impact. scripts/size-check.mjs — the --update branch; .github/workflows/ci.yml — matrix configuration for the size step Important Files Changed
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
scripts/size-check.mjs:98-107
When `--update` is called and one or more entries are already "OVER" budget, the script unconditionally writes the new (inflated) values and exits 0 — `failed` is set but never consulted in this branch. A developer who runs `pnpm size:update` to capture intentional shrinkage after a refactor could accidentally absorb an unrelated regression in the same pass, and CI would pass on the next push with no record that the budget grew. At minimum, the update path should print a warning (or exit non-zero) when it is ratcheting values **up** rather than down.
```suggestion
if (update) {
if (failed) {
console.warn('\nsize-check: WARNING — updating budget with one or more entries that grew past the old limit (OVER above). Commit intentionally.')
}
const next = {
_comment:
'Bundle-size budgets: max min+gzip BYTES per tree-shaken entry (scripts/size-check.mjs). The honest "lean install" number. Ratchet DOWN with `node scripts/size-check.mjs --update` when a build shrinks; CI fails if a build grows past budget*1.03.',
budgets: Object.fromEntries(Object.entries(results).map(([k, v]) => [k, { gzip: v.gzip, min: v.min }])),
}
writeFileSync(BUDGET_PATH, JSON.stringify(next, null, 2) + '\n')
console.log(`\nsize-check: budget written to ${BUDGET_PATH}`)
return
}
```
### Issue 2 of 2
.github/workflows/ci.yml:43-44
**Redundant size check across matrix nodes**
The bundle-size step runs on both the Node 22 and Node 24 matrix variants. esbuild output is deterministic and `zlib.gzipSync` at the default compression level should produce byte-identical results regardless of Node version, so the second run adds CI time without additional signal. If future zlib differences ever produced a different gzip length between versions, it could cause confusing pass/fail splits on the same commit. Consider either running the size check outside the matrix (a dedicated step or a separate job), or restricting it to one matrix leg with a `if: matrix.node == 22` condition.
Reviews (1): Last reviewed commit: "ci: bundle-size budget — tree-shaken min..." | Re-trigger Greptile |
| if (update) { | ||
| const next = { | ||
| _comment: | ||
| 'Bundle-size budgets: max min+gzip BYTES per tree-shaken entry (scripts/size-check.mjs). The honest "lean install" number. Ratchet DOWN with `node scripts/size-check.mjs --update` when a build shrinks; CI fails if a build grows past budget*1.03.', | ||
| budgets: Object.fromEntries(Object.entries(results).map(([k, v]) => [k, { gzip: v.gzip, min: v.min }])), | ||
| } | ||
| writeFileSync(BUDGET_PATH, JSON.stringify(next, null, 2) + '\n') | ||
| console.log(`\nsize-check: budget written to ${BUDGET_PATH}`) | ||
| return | ||
| } |
There was a problem hiding this comment.
When
--update is called and one or more entries are already "OVER" budget, the script unconditionally writes the new (inflated) values and exits 0 — failed is set but never consulted in this branch. A developer who runs pnpm size:update to capture intentional shrinkage after a refactor could accidentally absorb an unrelated regression in the same pass, and CI would pass on the next push with no record that the budget grew. At minimum, the update path should print a warning (or exit non-zero) when it is ratcheting values up rather than down.
| if (update) { | |
| const next = { | |
| _comment: | |
| 'Bundle-size budgets: max min+gzip BYTES per tree-shaken entry (scripts/size-check.mjs). The honest "lean install" number. Ratchet DOWN with `node scripts/size-check.mjs --update` when a build shrinks; CI fails if a build grows past budget*1.03.', | |
| budgets: Object.fromEntries(Object.entries(results).map(([k, v]) => [k, { gzip: v.gzip, min: v.min }])), | |
| } | |
| writeFileSync(BUDGET_PATH, JSON.stringify(next, null, 2) + '\n') | |
| console.log(`\nsize-check: budget written to ${BUDGET_PATH}`) | |
| return | |
| } | |
| if (update) { | |
| if (failed) { | |
| console.warn('\nsize-check: WARNING — updating budget with one or more entries that grew past the old limit (OVER above). Commit intentionally.') | |
| } | |
| const next = { | |
| _comment: | |
| 'Bundle-size budgets: max min+gzip BYTES per tree-shaken entry (scripts/size-check.mjs). The honest "lean install" number. Ratchet DOWN with `node scripts/size-check.mjs --update` when a build shrinks; CI fails if a build grows past budget*1.03.', | |
| budgets: Object.fromEntries(Object.entries(results).map(([k, v]) => [k, { gzip: v.gzip, min: v.min }])), | |
| } | |
| writeFileSync(BUDGET_PATH, JSON.stringify(next, null, 2) + '\n') | |
| console.log(`\nsize-check: budget written to ${BUDGET_PATH}`) | |
| return | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/size-check.mjs
Line: 98-107
Comment:
When `--update` is called and one or more entries are already "OVER" budget, the script unconditionally writes the new (inflated) values and exits 0 — `failed` is set but never consulted in this branch. A developer who runs `pnpm size:update` to capture intentional shrinkage after a refactor could accidentally absorb an unrelated regression in the same pass, and CI would pass on the next push with no record that the budget grew. At minimum, the update path should print a warning (or exit non-zero) when it is ratcheting values **up** rather than down.
```suggestion
if (update) {
if (failed) {
console.warn('\nsize-check: WARNING — updating budget with one or more entries that grew past the old limit (OVER above). Commit intentionally.')
}
const next = {
_comment:
'Bundle-size budgets: max min+gzip BYTES per tree-shaken entry (scripts/size-check.mjs). The honest "lean install" number. Ratchet DOWN with `node scripts/size-check.mjs --update` when a build shrinks; CI fails if a build grows past budget*1.03.',
budgets: Object.fromEntries(Object.entries(results).map(([k, v]) => [k, { gzip: v.gzip, min: v.min }])),
}
writeFileSync(BUDGET_PATH, JSON.stringify(next, null, 2) + '\n')
console.log(`\nsize-check: budget written to ${BUDGET_PATH}`)
return
}
```
How can I resolve this? If you propose a fix, please make it concise.| - name: Bundle-size budget (tree-shaken min+gzip) | ||
| run: pnpm size |
There was a problem hiding this comment.
Redundant size check across matrix nodes
The bundle-size step runs on both the Node 22 and Node 24 matrix variants. esbuild output is deterministic and zlib.gzipSync at the default compression level should produce byte-identical results regardless of Node version, so the second run adds CI time without additional signal. If future zlib differences ever produced a different gzip length between versions, it could cause confusing pass/fail splits on the same commit. Consider either running the size check outside the matrix (a dedicated step or a separate job), or restricting it to one matrix leg with a if: matrix.node == 22 condition.
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/ci.yml
Line: 43-44
Comment:
**Redundant size check across matrix nodes**
The bundle-size step runs on both the Node 22 and Node 24 matrix variants. esbuild output is deterministic and `zlib.gzipSync` at the default compression level should produce byte-identical results regardless of Node version, so the second run adds CI time without additional signal. If future zlib differences ever produced a different gzip length between versions, it could cause confusing pass/fail splits on the same commit. Consider either running the size check outside the matrix (a dedicated step or a separate job), or restricting it to one matrix leg with a `if: matrix.node == 22` condition.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
What
Measures the honest lean-install number — what a bundler ships for a real app (minified + gzipped), per tree-shaken entry point — and fails CI if a build grows past a committed budget (3% headroom; ratchet down with
pnpm size:updatewhen a build shrinks).Measured (min+gzip):
@ecsia/corealoneecsia is batteries-included, so it's larger than a minimal core like bitECS (~5 KB) — but the packages are
sideEffects: false, so a bundler drops relations / serialization / topics unless you import them. The budget guards against silent inflation, not toward a 5 KB target (that would fight the design).How
scripts/size-check.mjs: esbuild-bundles each entry against the built dist, min+gzips, compares tobundle-budget.json.--updateratchets.pnpm size/pnpm size:update; a CI step inbuild-test.Verification
pnpm build && pnpm size→ all three within budget. Fourth of the perf program (after codegen #82, bench lane #84).