Repository navigation
perf(console): move the types zod validators off the eager budgeted line - #10082
Conversation
`advancedChunks`'s `framework` group claims every `packages/types/` module wherever it is reached from, and that chunk is eager because `core` and `react` are reached from the entry. The nine `packages/types/src/zod/**` modules therefore rode the eager line although their only live consumer in the console bundle is `plugin-map`'s `ObjectMap.tsx`, which sits behind a lazy boundary: every page load fetched validators only the object-map view reads. They now have their own `types-zod` group, scoped to the directory with `includeDependenciesRecursively: false` — rolldown's default `true` absorbed three shared `packages/types/src/` modules whose runtime values the EAGER `plugin-grid` chunk reads, which kept the whole chunk eager and saved nothing. `scripts/vite-types-zod-lazy.ts` weighs the emitted chunks and fails the build in both directions: if the validators rejoin the eager closure, and if they stop being reachable from `plugin-map` at all. The second half is the one no byte ceiling can supply — deleted validators and deferred validators produce the same smaller, greener number. No ceiling, baseline or exemption constant moved. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Three conflicts, each resolved to keep BOTH rulings: - apps/console/vite.config.ts: this branch's `includeDependenciesRecursively: false` on `data-adapter` (objectui#9345) AND main's `types-zod` group at priority 82 (objectui#10065, #10082). The data-adapter note's anchored reading is relabelled as the historical reading it is. - scripts/check-eager-closure-budget.mjs: the membership half from this branch, and main's RETIRED exhausted-headroom block (objectui#10148) in place of the leg it replaced. - scripts/__tests__/check-eager-closure-budget.test.ts: this branch's options-capturing group parse, which subsumes main's `[,}]` tail and still sees `types-zod`; the parse note names both groups that carry an option. The membership declaration still says `packages/types` lands wholly in `framework`, which #10082 makes false by design; the next commit declares the split. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
…hip pin, exactly objectui#10082 moved `packages/types/src/zod/**` into a lazy `types-zod` group on purpose, so this branch's membership pin — `packages/types` lands WHOLLY in `framework` — is false by ruling after the merge. The pin now declares the split instead of dropping the package or loosening it. `PER_CHUNK_MEMBERSHIP_CARVE_OUTS` names each exception: a package-relative subtree of a declared package and the ONE chunk it must land in. A carved package is judged per directory: every module under the subtree in the carve-out's chunk, every other module in the declared chunk, nowhere else, and each side holding at least one module. That is what tells the healthy split apart from the failure #10082's first build produced — shared `src/` neighbours absorbed into `types-zod` along an import — which a per-package count reads as the same thing. The keys stay limited to budgeted chunks. A carve-out's destination may carry no ceiling only while it is absent from the SAME build's eager closure (read from `eager-closure.json`); a carve-out into an unbudgeted eager chunk is an error, since that is objectui#9345's incident written down as a ruling. Nested, self-targeting, undeclared-package and malformed carve-outs are errors too, as are a carved subtree or remainder holding nothing and directory counts that do not add up to the package counts. `chunk-membership.json` goes to v2 and adds `directories` (package -> package-relative directory -> chunk -> count), so a stale v1 artifact is refused. The emitter reads no declaration. A unit cross-check against the console config requires the carve-out group's own test to take the subtree and miss the rest of the package, the declared chunk's test to claim the subtree too, the carve-out group to outrank it, and to declare `includeDependenciesRecursively: false`. No ceiling, baseline or exemption constant moves. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
Fixes #10065
Clause-②: no — no⚠️ Stated as the gate's verdict rather than copied from a claim: this card carried no claim comment to copy the line from.
packages/*/srcpublished source moved.node scripts/check-changeset-presence.mjsexits 0 and prints "No source or published contract of a released package changed in this range, so no changeset is owed".The defect
advancedChunks'sframeworkgroup claims everypackages/types/module wherever it is reached from, and that chunk is eager becausecoreandreactare reached from the entry. So the ninepackages/types/src/zod/**validator modules rode the eager, budgeted line although their only live consumer in the console bundle ispackages/plugin-map/src/ObjectMap.tsx— which importsObjectMapConfigSchemafrom@object-ui/types/zodand sits behind a lazy boundary. Every page load fetched validators only the object-map view reads.Same defect class as objectui#9345, one tier over: a grouping rule deciding membership independently of reachability.
The premise, re-derived on this branch's own base (
0c2eb5eee)Probe and must-miss control, one command, over every emitted chunk:
0c2eb5eee)46083d761).describe('ObjectQL object name')fromobjectql.zod.tsassets/framework-C1GNpb8T.jsassets/types-zod-DUmm1NEb.js.describe('Action execution mode for chaining')fromcrud.zod.tsplugin-mapchunk in the eager closure?plugin-mapchunk statically imports the validators' chunk?frameworkfrom"./types-zod-DUmm1NEb.js"The composition, read from the build's own visualizer on the BEFORE leg: the nine modules are 140,541 rendered bytes of a 372,430-byte
frameworkchunk, led byobjectql.zod.ts,data-display.zod.tsandcomplex.zod.ts.frameworkchunk" was measured on objectui#9488's branch, whereframeworkheld 162 modules / 509,658 rendered bytes. Onorigin/mainthat chunk holds 108 modules / 372,430 rendered, so the same nine modules are 37.7% of it. The membership claim reproduced exactly; the share is leg-dependent and the card's number belongs to the other leg.⭐ What only the counterfactual could show: a SECOND consumer, created by the fix itself
The first counterfactual build failed, and it failed for a reason no source-level search can reach. Rolldown's
CodeSplittingGroup.includeDependenciesRecursivelydefaults totrue, so the new group captured the nine validators and recursively absorbed three shared modules —packages/types/src/data-display.ts,complex.ts,designer.ts. The eagerplugin-gridchunk reads runtime values out of those (ObjectGrid.tsximportsnormalizeTableColumnTypeandisSystemManagedFieldfrom@object-ui/types), and rolldown removes a higher-priority group's modules from the lower-priority ones, soframeworknever got them back.plugin-gridthen had to import the new chunk statically and all 140,541 bytes stayed eager — a saving of exactly nothing, dressed as a new chunk name.The guard printed it, naming both the co-tenant chunk and the source edge:
The repair is
includeDependenciesRecursively: falseon that one group, which is also what the group MEANS: the validators are what has no eager reader, not everything they happen to import. Their non-zod dependencies fall back toframework, where they were and where they belong.Measurement — both legs, one container, one command family
Per leg:
pnpm exec turbo run build --filter='./packages/*' --concurrency=2, thenpnpm --filter @object-ui/console build, thennode scripts/check-eager-closure-budget.mjs. Exit codes captured by redirect-then-read, never across a pipe. ⛔ No ceiling, baseline, allowance or floor constant was touched —MAX_EAGER_CLOSURE_GZIP_BYTES,PER_CHUNK_GZIP_CEILINGS,PER_CHUNK_BASELINE,EXHAUSTED_HEADROOM_ALLOWANCESandEXHAUSTED_HEADROOM_FLOOR_MULTIPLEare byte-identical on both legs.0c2eb5eee46083d761frameworkgzippedframeworkrawframeworkheadroom vs its unchanged 100,000 ceilingpnpm check:eager-closureexit⭐ The eager chunk COUNT is unchanged and the total is one higher — the bytes left the closure into a new lazy chunk rather than moving to another eager line.
ui-components(+8),data-adapter(+7) andplugin-grid(-21) are the only other rows that moved at all; that is import bookkeeping, and it is why the whole-closure delta (-27,776) and theframeworkdelta (-27,778) agree to 2 bytes.The saving against the card's bracket — it lands OUTSIDE it, above
The card priced the shrink as a bracket, 20,695 – 26,322 gzipped, deliberately not a point estimate, because nobody had built the counterfactual. This branch is the counterfactual.
The real figure is 27,778 gzipped — 1,456 bytes ABOVE the top of the bracket (5.5% over). Reported as a finding, not as a win: both estimators were computed against objectui#9488's
frameworkleg (95,454 gz over 509,658 rendered), and the share they divided by is the 27.6% that does not hold onmain. The bracket's ranking conclusion survives — the saving clears the 0.10x floor's 4,568-byte requirement by 6.1x — but its endpoints were anchored to the other leg and the top one is not an upper bound.What this does and does not do for objectui#9488
It restores headroom on the row that PR's⚠️ One interaction the reviewer should know about, stated rather than left to be discovered: objectui#9488 adds a chunk-membership half that asserts
Bundle Analysisjob exits 2 about. It does not land that PR, and it is not measured against it: this branch is based onorigin/main, and the two have not been built together.packages/typeslands wholly inframework. After this changepackages/typesis deliberately split acrossframeworkandtypes-zod, so whichever lands second owes that assertion an update. Nothing here touches that PR.The guard, and why it checks REACHABILITY first
scripts/vite-types-zod-lazy.tsruns ingenerateBundleon every console build — CI and local — and refuses a build in both directions:plugin-map. This is the failure that looks like a saving: if they are dropped from the graph (asideEffectsdeclaration that shakes too hard, an import narrowed toimport type, a group whose test stops matching), the eager closure gets smaller, every byte ceiling goes greener, and the object-map view throws at runtime for a schema that is not in the bundle. No byte ceiling can tell a deferral from a deletion. This one can, so it is reported first — an author told "the bundle grew" deletes more.Three counter-probes fail closed before either verdict: no chunk holds a validator module, no chunk holds a
plugin-mapmodule, or the eager walk cannot seereact-dom. Both verdicts are negative, so a matcher that matched nothing would agree with a correct bundle on both.Ablation ledger — red before, green after, restores verified
Every leg: mutate, prove on disk that the mutation landed (
grep -cof the removed text, expected 0), run, restore withgit checkout HEAD -- PATH, provegit diff HEADis empty. All four restores verified clean.includeDependenciesRecursively: falsefrom the groupkeeps the group from absorbing its own dependenciesviteTypesZodLazy()from the plugins arrayregisters the guard, or nothing weighs the bundle at allframework's 80routes the zod validators ... at a priority framework cannot tie+leaves no second claimantfinds the whole group table, not a fragment of it+ the routing caseLeg 4 is the reason
scripts/__tests__/check-eager-closure-budget.test.tsis in this diff at all. That file'sparseGroupsrequiredpriorityto be the group's last property, and the new group carries an option after it — so the group was silently absent from the attribution table while every case below went on passing. Measured before the widening: 42 groups parsed,types-zodnot among them. That is the parse's own "a matcher that matches nothing agrees with everything" note firing on itself.And the bundle-level half this change still gets for free: the first counterfactual build above is the guard's real red leg, on the real graph, with the real cause named.
Verification run
pnpm exec turbo run build --filter='./packages/*'pnpm --filter @object-ui/console build@object-ui/plugin-mapchunk(s)node scripts/check-eager-closure-budget.mjsnode scripts/check-eager-locale-catalogues.mjsenresident, 9 catalogues deferred — unchangednode scripts/check-changeset-presence.mjspnpm type-check:scriptspnpm check:unreferenced-sourcespnpm check:control-bytespnpm check:new-line-citationspnpm check:test-path-rootspnpm lint:rootpnpm --filter @object-ui/console lintpnpm exec vitest run scripts/NOT MEASURED, declared rather than skipped silently:
pnpm check:docs-route-closure— it weighsapps/site's own route closure and needs that app's build; nothing in this diff reaches it. And the object-map view was verified structurally (the emittedplugin-mapchunk carriesfrom"./types-zod-DUmm1NEb.js", and theObjectQL object nameliteral resolves inside that chunk) rather than in a browser.Out of scope, named rather than taken
PER_CHUNK_GZIP_CEILINGS.frameworknow stands 58,218 bytes (0.64x the 89 KiB regression) above the live measurement, up from 0.33x. Re-pinning it down is the honest follow-up and would restore the row's resolution — but a ceiling edit is exactly what this card was forbidden to make, and it is a deliberate re-pin for the seat, not an in-lane change.PER_CHUNK_BASELINE.framework(72,245) is now far above the measurement it is compared against by hand. That constant is a historical reading anchored to its own build and is not read by the sensitivity leg, so it is stated, not moved.Generated by Claude Code