crust maps your App Router project from source, joins it to the build output, and keeps a snapshot per build — so a regression traces back to the commit and the import that caused it.
1$ next build && npx crust diff main
2
3crust diff cfdcf500 → 4a802397
4
5/products/[slug] 543.2 kB +0.0 kB
6 shell 100% → 45%
7 ✂ <ProductGallery> left the shell
8 uncached fetch at lib/http.ts:3
9
102 budget breaches · exit 1
1{
2 "defaultFirstLoadBytes": 250000,
3 "firstLoadBytes": {
4 "/products/[slug]": 180000
5 },
6 "maxGrowth": 0.05,
7 "defaultMinShellRatio": 0.6
8}
9
10// bytes, growth and shell ratio — a check that
11// only guards bytes passes a halved shell
1'use client'
2
3import { useEffect } from 'react'
4
5export function CrustDevtools() {
6 useEffect(() => {
7 if (!process.env.NEXT_PUBLIC_CRUST) return
8 import('crust/widget').then((m) => m.mountCrustWidget())
9 }, [])
10
11 return null
12}
Read the build, predict the shell, check the prediction, then compare against last time.
/dashboard — static shell is 39%, below the 60% floorcookies() at app/dashboard/page.tsx:18/products/[slug] — static shell is 45%, below the 60% flooruncached fetch at lib/http.ts:3No build error was produced and no bundle grew. A use cache directive was removed three call frames below the page, and 55% of the route silently stopped being static.
A CLI, a snapshot store you can commit, a GitHub Action, an HTML report and an in-page widget.
Every byte of first-load JS traced to the file that produced it — through barrel files, monorepo packages and both bundlers.
What is prerendered, what is postponed, and the exact call site that pushed each component out of the shell.
A snapshot per build, keyed on more than the git SHA. Diffs name the module responsible, not just the route that grew.
Budgets on bundle size and shell ratio. The PR comment updates in place instead of stacking on every push.
Web Vitals, long animation frames, an image audit and the streaming waterfall — behind a build-time gate.
Anything not lexically resolvable is reported as unknown. A predictor that guesses wrong poisons every number beside it.
Permanent, and written down before the first user arrived.
crust reads a production build. It refuses to measure dev output, because dev numbers are fiction.