Filed bare (no assignee, no domain:*, no pm:*) out of the measurement-first round on objectui#7848, whose fence says a slimming that the census finds "is a second card and a second PR". This is that card. ⛔ Nothing here has been implemented and ⛔ no gate constant was moved.
All figures below are from four full console builds in one container on tip 1f4e02995a, read directly out of apps/console/dist/eager-closure.json. Every mutation was proved on disk (blob-hash before/after plus printed grep anchors) and restored with git checkout HEAD -- path, proved by an empty git diff HEAD.
The reading on 1f4e02995a
ui-components 395,032 gz ceiling 399,000 headroom 3,968 = 0.0435x the 91,136 B regression
aggregate 3,186,729 gz ceiling 3,210,000 headroom 23,271 = 0.2554x
ui-components is the only entry in EXHAUSTED_HEADROOM_ALLOWANCES (4,289). The gate itself prints the trip point: reds below 3378 bytes — so the live row has 590 bytes of runway before the ratchet fires. Clearing the 0.10x floor on its own needs the chunk at 389,886 or less, i.e. a saving of 5,146 gzipped bytes.
Where the bytes are
Census of the emitted chunk's own module list (2,240 modules, 2,926,967 B renderedLength):
| area |
renderedLength |
share |
modules |
lucide-react |
1,480,797 |
50.6% |
1,770 |
packages/components/src/renderers |
396,868 |
13.6% |
105 |
packages/fields/src/widgets |
280,718 |
9.6% |
72 |
packages/components/src/ui |
112,206 |
3.8% |
47 |
packages/components/src/custom |
104,106 |
3.6% |
29 |
react-day-picker |
99,431 |
3.4% |
98 |
react-hook-form |
80,343 |
2.7% |
1 |
Half the chunk is lucide, through two independent doors.
Door 1 — the dynamic-import map is EAGER (dynamicIconImports.mjs, 263,547 B rendered)
Four non-test modules statically import lucide-react/dynamic.mjs:
packages/components/src/lib/lazy-icon.tsx:24
packages/app-shell/src/views/metadata-admin/widgets.tsx:46
packages/app-shell/src/utils/getIcon.ts:16
apps/console/src/utils/getIcon.ts:11
The map is 1,767 () = arrow entries whose only job is to be called later. It does not need to be in the first payload.
Measured (ablation: the four imports stubbed, icons record left alone):
ui-components 395,032 -> 349,283 -45,749
aggregate 3,186,729 -> 3,141,057 -45,672
eager/total chunks 51/528 -> 51/528 (unchanged)
⭐ The two deltas agree within 77 bytes, so this is bytes LEAVING the page load, not bytes moving between columns. No new chunk, no extra request. ui-components headroom becomes 49,717 = 0.55x — above the 0.10x floor and still under 1.00x, so the ceiling stays sensitive rather than going blind. This one change alone is 8.9x the 5,146 bytes the row needs.
Door 2 — the whole 1,767-icon record is EAGER
packages/components/src/renderers/action/resolve-icon.ts:9
import { icons, type LucideIcon } from 'lucide-react';
icons is lucide's runtime record, published as export { index as icons } over a namespace import of a 1,767-line barrel. A namespace object has no dead members, so tree-shaking cannot help: every icon module is statically reachable. Verified on the emitted artifact rather than from source — the path data of air-vent, an icon this product does not use, appears exactly once in assets/ui-components-*.js.
It is also the ONLY non-test module that imports the record (the three other hits are test files), which matches check-lucide-icon-record-names.mjs's declared census of "this file alone".
Measured (ablation: resolveIcon returns null, record import removed):
ui-components 395,032 -> 254,749 -140,283
aggregate 3,186,729 -> 3,188,038 +1,309 (WORSE)
eager/total chunks 51/528 -> 176/2138
⚠️ The aggregate gets worse, and the cause is the co-tenancy mechanism objectui#6680 documented. Diffing the chunk membership shows eight icon modules, well under 3 KB, were placed inside three previously-lazy plugin chunks, which drags all of them eager:
plugin-dashboard + arrow-up-right.mjs -> 49,182 gz eager
plugin-gantt + file-down, minimize-2, panel-left-close,
wand-sparkles, zoom-in, zoom-out -> 36,561 gz eager
plugin-report + table-2.mjs -> 10,417 gz eager
Adding one advancedChunks group per icon — the shape objectui#7479 used for the ten locale catalogues, and for the same stated reason — removes that:
ui-components 395,032 -> 250,671 -144,361
aggregate 3,186,729 -> 3,126,208 -60,521
eager/total chunks 51/528 -> 328/2294
⚠️ The price is 278 eager per-icon chunks summing 77,024 gz, i.e. 278 extra requests on first paint, and the ui-components headroom becomes 148,329 = 1.63x, which trips the gate's blindness leg in the other direction and would need the ceiling and its baseline re-pinned DOWN together. Not proposed as-is; the shape worth designing is one shared chunk for the eagerly-used icons plus per-icon chunks for the rest.
Ranked
- Door 1, lazy-load
lucide-react/dynamic.mjs. -45,749 on the row, -45,672 on the aggregate, zero new chunks, ceiling stays sensitive. Clears the floor on its own. ⚠️ The number above is an ablation, so it is an UPPER bound: a real implementation keeps an async loader and some cheaper iconNames membership predicate.
- Door 2, retire the
icons-record import in favour of the async resolver lazy-icon.tsx already implements. Bigger, but needs the chunking design first and a ceiling/baseline re-pin after. ⚠️ Contract change: resolveIcon is a maintainer-ruled synchronous seam (objectui#5935, ruling of 2026-09-03) returning LucideIcon | null, and four surfaces depend on their own fallbacks. That ruling has to be revisited before this is touched.
- ⛔ Not the
packages/fields barrel. scripts/vite-ineffective-dynamic-imports.ts already measured barrel surgery at +329 bytes and the maintainer ruled option C on 2026-08-22. Its option A is still on hold and is a prerequisite, not an alternative.
- ⛔ Not a re-chunk of
react-day-picker / react-hook-form / vaul / date-fns / embla-carousel / react-resizable-panels. These are in ui-components only because they match no vendor group; moving them shrinks the column without removing a byte, and the console config already rules on that in its own words: "This moves NO MODULE BYTES ... Nothing here may be read as headroom that was earned."
Reproduction
pnpm install --frozen-lockfile
pnpm turbo run build --filter='./packages/*'
pnpm --filter @object-ui/console build
node -e "const r=require('./apps/console/dist/eager-closure.json');console.log(r.eagerGzipBytes, r.files.find(f=>f.name==='ui-components').gzipBytes)"
Generated by Claude Code
Filed bare (no assignee, no
domain:*, nopm:*) out of the measurement-first round on objectui#7848, whose fence says a slimming that the census finds "is a second card and a second PR". This is that card. ⛔ Nothing here has been implemented and ⛔ no gate constant was moved.All figures below are from four full console builds in one container on tip
1f4e02995a, read directly out ofapps/console/dist/eager-closure.json. Every mutation was proved on disk (blob-hash before/after plus printed grep anchors) and restored withgit checkout HEAD -- path, proved by an emptygit diff HEAD.The reading on
1f4e02995aui-componentsis the only entry inEXHAUSTED_HEADROOM_ALLOWANCES(4,289). The gate itself prints the trip point:reds below 3378 bytes— so the live row has 590 bytes of runway before the ratchet fires. Clearing the 0.10x floor on its own needs the chunk at 389,886 or less, i.e. a saving of 5,146 gzipped bytes.Where the bytes are
Census of the emitted chunk's own module list (2,240 modules, 2,926,967 B renderedLength):
lucide-reactpackages/components/src/rendererspackages/fields/src/widgetspackages/components/src/uipackages/components/src/customreact-day-pickerreact-hook-formHalf the chunk is lucide, through two independent doors.
Door 1 — the dynamic-import map is EAGER (
dynamicIconImports.mjs, 263,547 B rendered)Four non-test modules statically import
lucide-react/dynamic.mjs:The map is 1,767
() =arrow entries whose only job is to be called later. It does not need to be in the first payload.Measured (ablation: the four imports stubbed,
iconsrecord left alone):⭐ The two deltas agree within 77 bytes, so this is bytes LEAVING the page load, not bytes moving between columns. No new chunk, no extra request.
ui-componentsheadroom becomes 49,717 = 0.55x — above the 0.10x floor and still under 1.00x, so the ceiling stays sensitive rather than going blind. This one change alone is 8.9x the 5,146 bytes the row needs.Door 2 — the whole 1,767-icon record is EAGER
packages/components/src/renderers/action/resolve-icon.ts:9iconsis lucide's runtime record, published asexport { index as icons }over a namespace import of a 1,767-line barrel. A namespace object has no dead members, so tree-shaking cannot help: every icon module is statically reachable. Verified on the emitted artifact rather than from source — the path data ofair-vent, an icon this product does not use, appears exactly once inassets/ui-components-*.js.It is also the ONLY non-test module that imports the record (the three other hits are test files), which matches
check-lucide-icon-record-names.mjs's declared census of "this file alone".Measured (ablation:
resolveIconreturns null, record import removed):Adding one
advancedChunksgroup per icon — the shape objectui#7479 used for the ten locale catalogues, and for the same stated reason — removes that:ui-componentsheadroom becomes 148,329 = 1.63x, which trips the gate's blindness leg in the other direction and would need the ceiling and its baseline re-pinned DOWN together. Not proposed as-is; the shape worth designing is one shared chunk for the eagerly-used icons plus per-icon chunks for the rest.Ranked
lucide-react/dynamic.mjs. -45,749 on the row, -45,672 on the aggregate, zero new chunks, ceiling stays sensitive. Clears the floor on its own.iconNamesmembership predicate.icons-record import in favour of the async resolverlazy-icon.tsxalready implements. Bigger, but needs the chunking design first and a ceiling/baseline re-pin after.resolveIconis a maintainer-ruled synchronous seam (objectui#5935, ruling of 2026-09-03) returningLucideIcon | null, and four surfaces depend on their own fallbacks. That ruling has to be revisited before this is touched.packages/fieldsbarrel.scripts/vite-ineffective-dynamic-imports.tsalready measured barrel surgery at +329 bytes and the maintainer ruled option C on 2026-08-22. Its option A is still on hold and is a prerequisite, not an alternative.react-day-picker/react-hook-form/vaul/date-fns/embla-carousel/react-resizable-panels. These are inui-componentsonly because they match no vendor group; moving them shrinks the column without removing a byte, and the console config already rules on that in its own words: "This moves NO MODULE BYTES ... Nothing here may be read as headroom that was earned."Reproduction
Generated by Claude Code