This site uses one functional cookie to keep feature rollouts consistent for you. Nothing is set until you choose. See the privacy notice.
Dev notes
How @next/bundle-analyzer exposed Auth0's client SDK shipping to the browser for no reason.
@next/bundle-analyzer, wrap next.config.ts, run ANALYZE=true next build --webpack--webpack flag is required; Next.js 16 defaults to Turbopack which the analyzer doesn't supportproxy.js edge bundle, Auth0 packages in the client bundle, and date-fns at 24 modulesproxy.js is the middleware (intentionally large: it runs auth and CSP on every request). date-fns at 24 modules is correct tree-shaking — only the calendar's named imports are includedRoot layout wrapped the whole app in <Auth0Provider>, a React context so client components could call useUser().
useUser() had zero call sites in the entire codebase. Auth0Provider pulled in jose (JWT parsing, hundreds of KB), oauth4webapi, openid-client, and swr — server-only libraries with no business in a browser bundle.
Auth0Provider from layout.tsx — three lines deleted, layout became a synchronous functionjose, oauth4webapi, openid-client, and swr disappeared from the client bundle entirelyauth0.getSession() directly; API routes call auth0.getAccessToken()useUser — zero results — confirmed the removal was safe before shippingUpdate — August 15, 2026
Everything above is a bundle audit done by hand, and the honest weakness of a hand audit is that it only holds until the next dependency lands. There is a budget now: gzipped first-load JS per route plus the shared baseline every route pays, checked by pnpm size against numbers committed next to the check.
The numbers came out of a real build rather than out of an ambition. A budget set to where you wish the bundle were is red on the day it lands, and a check that is red on arrival teaches everyone to scroll past it. So each one is the measured figure plus about seven percent of headroom, and a test fails if a budget is ever edited below its own recorded baseline. The 3D pages are budgeted generously instead of excluded, because an excluded route can regress without limit and /world doubling is exactly the thing worth catching. A catch-all ceiling means a new route cannot land unbudgeted.
Getting the numbers took more care than expected. This app builds with Turbopack, so .next/app-build-manifest.json does not exist and the build no longer prints a first-load column, which is what most write-ups on this tell you to read. The sizes are derived instead from the build manifest and each route’s RSC manifest — and a derivation is a guess until it is checked, so it is validated against ground truth: for a prerendered route the built HTML lists the script tags a browser actually fetches, and for /design-system the derived set and the real HTML agree on all eighteen files with nothing extra on either side.
The same budget on the sibling Angular app found real fat within a day. Its initial bundle was over its own limit, and the tempting fix is to raise the limit — which turns a budget into a rubber stamp. So I pulled the build’s metafile and attributed every byte instead: the bundle was framework plus one file, sixty kilobytes of essay prose, third largest input in the whole app. It was eager because three services import the thoughts data for slugs, titles and tags, and not one of them ever reads the body. Every first-paint visitor was downloading twenty essays to render a desktop that shows none of them. Moving the prose behind the lazy route took it from 428KB to 373KB and put it back under the existing limit, which stayed exactly where it was. A budget you raise to meet reality measures nothing; a budget that makes you go and look is worth having.
Where the check runs turned out to be the harder call. A budget needs a build, and the quality job deliberately does not build, so putting it there would add minutes to every push. It runs in the smoke job instead, which already builds. My first version left it as an ordinary step after the browser tests, which quietly coupled two unrelated facts: a bundle regression and a failing assertion. This suite has gone red before because a third-party API stopped answering, and under that arrangement an outage would have hidden a real size regression behind an unrelated failure.
It runs on always() now, the same condition the report upload already used, because the build output is on disk whether the assertions passed or not. That opens exactly one new case: when the build is what failed there is nothing to measure, and a second red step blaming the bundle would point at the wrong thing. So the check takes a flag in CI and stands down with a plain message in that one case, while still failing loudly when I run it locally without building — because there, the missing build is the mistake.