Skip to content

ci: bundle-size budget — tree-shaken min+gzip, CI-gated#85

Merged
andymai merged 1 commit into
mainfrom
ci/bundle-size-budget
Jun 8, 2026
Merged

ci: bundle-size budget — tree-shaken min+gzip, CI-gated#85
andymai merged 1 commit into
mainfrom
ci/bundle-size-budget

Conversation

@andymai
Copy link
Copy Markdown
Owner

@andymai andymai commented Jun 8, 2026

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:update when a build shrinks).

Measured (min+gzip):

import gzip note
kernel (world + systems + scheduler) 40 KB the typical app
@ecsia/core alone 30 KB world + components
full umbrella 55 KB everything (upper bound)

ecsia 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 to bundle-budget.json. --update ratchets.
  • pnpm size / pnpm size:update; a CI step in build-test.
  • Honest bundle-size note on the performance page (+ refreshed the regression-guard section to mention the new ratio-based bench lane from the sibling PR).

Verification

pnpm build && pnpm size → all three within budget. Fourth of the perf program (after codegen #82, bench lane #84).

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.
@andymai andymai enabled auto-merge (squash) June 8, 2026 08:48
@andymai andymai merged commit d85227e into main Jun 8, 2026
9 checks passed
@andymai andymai deleted the ci/bundle-size-budget branch June 8, 2026 08:49
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Jun 8, 2026

Greptile Summary

This PR adds a tree-shaken min+gzip bundle-size gate: a new scripts/size-check.mjs script bundles three representative entry points with esbuild, gzips the output, and compares against committed byte limits in bundle-budget.json. A new CI step in build-test fails the build if any entry grows more than 3% past its budget.

  • scripts/size-check.mjs: measures kernel, @ecsia/core-only, and full-umbrella entries; the --update ratchet unconditionally writes new budgets (including inflated values) without warning when entries are "OVER".
  • .github/workflows/ci.yml: adds pnpm size to the build-test matrix, which runs the deterministic check redundantly on both Node 22 and Node 24.
  • website/guide/performance.md: refreshes the regression-guard section and adds an accurate bundle-size summary.

Confidence Score: 4/5

The 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

Filename Overview
scripts/size-check.mjs New bundle-size measurement script; the --update path absorbs OVER-budget entries without a distinguishing warning or non-zero exit
bundle-budget.json New committed budget file with per-entry gzip+min byte counts for kernel, core-min, and full-umbrella; looks correct
.github/workflows/ci.yml Adds bundle-size CI step to build-test job; runs on both Node 22 and Node 24 matrix, making the check redundant on the second runner
package.json Adds size and size:update scripts; esbuild is already a dev dependency
website/guide/performance.md Refreshes regression-guard section and adds a new bundle-size section with accurate numbers; no issues found

Fix All in Claude Code

Prompt To Fix All With AI
Fix 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

Comment thread scripts/size-check.mjs
Comment on lines +98 to +107
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
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Suggested change
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.

Fix in Claude Code

Comment thread .github/workflows/ci.yml
Comment on lines +43 to +44
- name: Bundle-size budget (tree-shaken min+gzip)
run: pnpm size
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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!

Fix in Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant