← Writing

Cutting a bundle by 83%

18 Jun 2026 · Performance

The admin app loaded 10.4 MB of JavaScript before it rendered anything useful. On the office wifi nobody noticed. On an agent's phone, on mobile data, in a building with one bar, it was unusable.

It ships 1.78 MB now. Most of the difference came from two changes.

Measure before guessing

The instinct is to start deleting dependencies. That's usually wrong — you spend an afternoon shaving 40 KB off a date library while a 3 MB import sits untouched three routes away.

Look at the actual composition of the bundle first. The thing that's killing you is almost never the thing you assumed.

Everything was in one chunk

The app used TanStack Router inside a Next.js route. Every route component was imported eagerly at the top of the router definition, so the router file pulled in the entire application — every page, every modal, every chart library — and all of it landed in the first chunk.

A visitor opening the listings page downloaded the reporting dashboard, the settings screens, and every editor they might never touch.

Lazy route definitions fix this. The router only needs to know a route exists to match a URL; it doesn't need the component until something navigates there.

Split on navigation, not on components

The second pass was route-level code splitting. Not React.lazy sprinkled over individual components, but splitting at the boundary where users actually change context.

Component-level splitting tends to produce a lot of tiny chunks and a waterfall of requests. Route-level splitting matches how people move through an app: they land somewhere, do a task, go somewhere else. Each of those transitions is a natural place to fetch more code.

What I'd do differently

I'd have set a budget on day one. The bundle didn't reach 10.4 MB through one bad decision — it got there through a hundred reasonable ones, none of which looked expensive on its own.

A CI check that fails the build when first-load JS crosses a threshold would have caught it while it was still a 200 KB problem instead of a 10 MB one.