Skip to main content
crust separates regressions from ceilings. A regression compares this build with the project’s own baseline: a route became less static, a read stopped being cached, a shell disappeared, or first-load JavaScript grew. The first three fail CI automatically. JavaScript growth is reported in the diff but fails CI only when maxGrowth is configured. A ceiling describes one build in isolation: a route may not exceed 250 kB or fall below a 60% shell ratio. That distinction determines which checks need configuration.

The comparison decision

crust diff and crust ci summarize the same evidence with one of four decisions:
  • BLOCK - an automatic regression or configured budget was breached
  • REVIEW - meaningful movement needs judgment but is not enforceable
  • CLEAR - the comparison found no blocking regression
  • CANNOT DECIDE - missing or incompatible evidence makes a reliable delta comparison impossible
diff reports that decision without enforcing it. ci exits non-zero only for blocking breaches. The verdict is followed by regressions and improvements, their strongest grouped cause, attribution coverage, and the likely next action. Absolute ceilings describe the current build, so they still run without a comparable baseline. A ceiling breach is therefore BLOCK even when route-to-route deltas cannot be decided.

Automatic regression checks

As soon as a comparable baseline exists, crust ci fails without a budget file when:
  1. a route moves down the rendering-mode scale
  2. a new uncached-read reason appears
  3. a route that previously emitted a measurable shell emits none
ROUTE_HANDLER is not on this scale because a handler has no page shell. unknown is not on the scale because placing it anywhere would turn missing evidence into a claim.

Explicit ceilings

These rules require .perf/budgets.json because crust cannot know the right limits for another project:
  • maximum first-load bytes globally or per route
  • maximum percentage growth against a baseline
  • minimum static-shell ratio globally or per route
Ceilings still run when there is no baseline or when two baselines are incomparable. They describe the current build, so no delta is needed.

When crust does not call a regression

A newly added route

Its baseline size is mathematically zero, but calling its entire bundle “growth” would make every new page a regression. New routes are reported as additions and can still breach an absolute first-load ceiling.

An unknown rendering mode

If either side is unknown, the change is shown but has no direction and does not fail the rendering-mode check.

Incomparable builds

crust does not compute enforceable deltas when any of these changed:
  • bundler
  • Next.js major version
  • snapshot schema version
The PR comment names the incompatibility and omits route-level causes. Comparing across those boundaries would attribute framework or measurement changes to the application PR.

Build configuration changed

crust records configuration that materially changes emitted output. crust analyze, crust diff, and the PR comment all report these changes separately from route deltas, whenever a compatible local baseline exists:
  • Cache Components
  • bundler, Next.js version, and Node major
  • browser source-map emission
  • partialPrefetching and the instantInsights validation level, on Next 16.3
  • relevant experimental flags
  • per-route dynamic, revalidate, runtime, fetchCache, instant, and prefetch
A bundler, Next major, snapshot schema, or Cache Components change makes route deltas unsafe and stops regression enforcement. The comment names each one with what it explains, so “no deltas” never reads as “nothing happened”. Other changes remain comparable and are retained as configuration evidence: the comment lists them in a note, states what each accounts for, and says outright that they were not caused by application code. A route that regressed because it declared dynamic, revalidate, runtime, or fetchCache still fails - it is a real staticness downgrade - but the comment names the declaration instead of sending the reviewer to look for a cause that is not in the code.

Instant Navigations

Next 16.3 adds the partialPrefetching config flag, an instantInsights validation level, and the instant and prefetch route segment exports. crust records all four and reports them here, as declared intent. partialPrefetching is kept raw, so 'unstable_eager' stays distinct from true. The segment exports are inherited from layouts like every other segment key. They report as configuration rather than as per-route regressions because a flag flip is one decision, not one defect per route. crust does not claim to have measured instant navigation, and cannot. Two next@16.3.0 builds of one app differing only in partialPrefetching emit an identical artifact tree, a byte-identical server/prefetch-hints.json, and per-segment payloads within a few bytes of each other; instant appears in no build manifest. Next enforces the contract by failing the build, so a route that stopped being instant does not produce a comparable snapshot to regress against - it produces a red build. One transition is named weakened: dropping from an experimental-error validation level to a warning level. At an error level, a route that breaks its instant contract fails the build. Below it, the same route ships. That change is silent in the build output and survives a green CI run, which is the reason this axis is worth reporting at all.

Byte noise

Chunk filenames are content-hashed, so tiny byte changes can appear even when behavior did not. crust records exact totals but does not classify a first-load delta of 512 bytes or less as a regression in diff severity or terminal grouping. This display floor is not a CI threshold; JavaScript growth is enforced only through maxGrowth.

Intentional downgrades

Use allowRegression when making a route less static is deliberate:
.perf/budgets.json
The exemption applies only to crust’s automatic mode, cache, and shell-disappearance checks. Explicit firstLoadBytes, maxGrowth, and shell-ratio ceilings continue to apply.
Keep exemptions route-specific. A global switch would let one intentional dynamic page disable protection for the rest of the application.

How a cause is selected

For each regressed route, crust prefers evidence in this order:
  1. a newly postponed component with a call site
  2. a newly introduced uncached read
  3. the reason for a rendering-mode drop
  4. the largest changed first-party module
  5. unknown, with the missing evidence stated
It does not substitute the largest framework package when no application module can be proven. Blame that looks precise but cannot be acted on is worse than an explicit unknown.