Skip to content

inspector: cut first paint by 30% and delete the unused half of packages/ui - #35

Open
Mark-Life wants to merge 3 commits into
mainfrom
atm/task-019fe47f-da41-78a0-9962-b005e49dbd69
Open

inspector: cut first paint by 30% and delete the unused half of packages/ui#35
Mark-Life wants to merge 3 commits into
mainfrom
atm/task-019fe47f-da41-78a0-9962-b005e49dbd69

Conversation

@Mark-Life

Copy link
Copy Markdown
Owner

Problem:

The inspector fetched its entire JavaScript bundle before painting a pixel — 332.6 KiB gzip, including a charting library only the memory route renders, a date picker only a popover opens, and a MessagePack codec the transport never uses. Nothing was code-split, so dist/ and the first paint were the same number.

manualChunks in vite.config.ts ended with a catch-all return "vendor" for anything under node_modules. A named chunk is reachable from the entry, so that line pinned every dependency to the first paint, and would have silently undone any lazy() boundary added above it: the component splits, its dependencies do not.

Separately, packages/ui carried 56 shadcn components, 28 of which no file in the repo imports. They cost nothing in JS, but globals.css scans packages/ui/src/** for class names, so each contributed utilities to both apps' stylesheets.

Solution:

Name only the trees the first paint pulls in whole (effect, react-dom, lucide-react) and leave the rest to Rolldown, which keeps a lazy-only module in the async chunk that needs it. Four boundaries then carry weight: the three non-default routes, SessionDetail inside the sessions route, the Calendar behind the date facet, and the Toaster. sessions stays eager because an empty hash resolves to it. Each boundary has a sized Suspense fallback, so no pane collapses while its chunk arrives.

@effect/rpc serves NDJSON and MessagePack from one module and imports msgpackr statically; src/stubs/msgpackr.ts is aliased in its place and throws if MessagePack is ever constructed. The CLI's server keeps the real package.

Deleting the 28 unimported components takes the inspector's CSS from 162.8 KiB to 115.0 and the web app's from 162.0 to 113.8, and drops eight dependencies only reachable through them. shadcn add <name> brings any of them back. Two dead @source globs went too — they pointed at packages/apps and packages/components, neither of which exists.

Full numbers and method in docs/bundle-size.md.


Security Impact:

None. The msgpackr alias is browser-build-only and narrows an unused deserializer to a throwing constructor.


Testing:

bun run typecheck, bunx --bun ultracite check, and bun test:

Tasks:    9 successful, 9 total     (typecheck)
Checked 255 files in 2s. No fixes applied.
208 pass, 2 fail                    (bun test)

The two failures are in packages/core/test/agents.test.ts and reproduce unchanged on main.

No browser exists in this environment, so the lazy boundaries were exercised under happy-dom: the shell, all three lazy routes, the Toaster (fired a toast to force it to paint) and the Calendar inside its Radix portal all mounted. That harness needs a global DOM registration that breaks the 28 non-DOM tests sharing the process, so it was not kept. peektrace serve was also run against the new dist/; all 20 assets return 200.

Before, first paint was 1159.5 KiB raw / 332.6 KiB gzip and equalled the whole directory. After, it is 801.4 / 233.4, with 291 KiB deferred and 66.8 KiB genuinely deleted.

Mark-Life and others added 3 commits August 9, 2026 17:37
`manualChunks` ended with a catch-all `return "vendor"` for anything under
`node_modules`. A named chunk is reachable from the entry, so that line pinned
every third-party dependency to the first paint — and would have silently
undone any `lazy()` boundary added above it: the component splits, its
dependencies do not. Only trees the first paint pulls in whole are named now;
the rest is left to Rolldown.

With that fixed, four boundaries carry their weight: the three non-default
routes, `SessionDetail` inside the sessions route, the `Calendar` behind the
date facet's popover, and the `Toaster`. `sessions` stays eager because an
empty hash resolves to it.

`@effect/rpc` serves NDJSON and MessagePack from one module and imports
msgpackr statically, so 27 KB of MessagePack shipped whichever serialization
the app built. `src/stubs/msgpackr.ts` is aliased in its place and throws if
MessagePack is ever constructed; the transport is NDJSON.

First paint: 1159.5 KiB raw / 332.6 KiB gzip -> 801.4 / 233.4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`packages/ui` carried 56 shadcn components; 28 had no importer anywhere in the
repo, `demo.tsx` included. They cost nothing in JS, but `globals.css` scans
`packages/ui/src/**` for class names, so each one contributed utilities to both
apps' stylesheets. The inspector's CSS drops 162.8 KiB -> 115.0 (24.6 -> 18.5
gzip); the web app's 162.0 -> 113.8.

Two `@source` globs went with them. `../../../apps/**` and
`../../../components/**` resolve against `packages/ui/src/styles`, so they
pointed at `packages/apps` and `packages/components`, neither of which exists.
Removing them leaves a byte-identical stylesheet.

Eight dependencies were only reachable through the deleted components:
`@base-ui/react`, `cmdk`, `date-fns`, `embla-carousel-react`, `input-otp`,
`react-resizable-panels`, `vaul`, `zod`.

`shadcn add <name>` brings any component back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Method, before/after numbers, what each boundary defers, and what is left.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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