Skip to main content
crust compares two Next.js App Router production builds and leads with the decision: what changed, why it changed, everything affected, and whether the evidence supports merging or shipping it. A one-build analyzer answers “what is in this bundle?” crust answers “what changed between builds, why, and can it ship?”

How crust builds the comparison

crust does not crawl a deployed URL, run Lighthouse, or estimate from next dev. It joins each completed .next build with the application’s source graph and stores a compatible snapshot. crust diff [base] [head] then derives one decision from rendering, caching, shell, and client-cost movement. Two named refs are compared from stored snapshots, so nothing is rebuilt and neither side has to be checked out. When a ref has not been measured yet, crust diff <base> <head> --build records it - each tip built in its own detached git worktree, both snapshots written to the project’s .perf/, your checkout untouched. It reads:
  • route, prerender, client-reference, build, and resolved-config manifests
  • emitted HTML from server/app/**/*.html
  • production JavaScript from static/**/*.js and its source maps
  • pages, layouts, components, and first-party imports under app/ or src/app/
Source supplies the why; emitted build artifacts supply the what.

Function-level analysis

crust parses source without executing it. It looks for:
  • cookies, headers, draftMode, connection, and page searchParams
  • fetch() caching, no-store, default uncached reads under Cache Components, and 'use cache'
  • route exports such as dynamic, revalidate, runtime, and fetchCache, plus instant and prefetch on Next 16.3
  • Suspense and client boundaries, rendered components, imports, exports, aliases, and barrel re-exports
It follows reachable exported functions and imported bindings from each page and layout to the dynamic read or attributed module. Direct imports can be narrowed per function. Namespace imports, computed calls, export *, unresolved aliases, and components passed through props fall back to conservative module-level evidence or remain unknown.
Every cause is labeled verified, inferred, or unknown. File-level byte attribution needs productionBrowserSourceMaps: true; route totals and shell analysis do not.

Compared with Next.js Bundle Analyzer

The experimental next experimental-analyze is an interactive Turbopack module explorer. Use it to inspect the client and server module graph of one build, find large dependencies, and follow their import chains. Next.js also provides @next/bundle-analyzer for webpack bundle inspection. crust complements it by reading completed webpack or Turbopack builds, recording compatible snapshots, and comparing them automatically. It analyzes rendering modes, cache decisions, static-shell changes, shared causes, and client-JavaScript growth, then turns regressions into CI findings and merge verdicts.
The Next.js analyzer answers “what is in this bundle?” crust focuses on “what changed since the baseline, why did it change, and should this PR merge?”

What crust answers

Across two builds:
  • Should this change be blocked, reviewed, cleared, or left undecidable?
  • Did a route become less static, lose caching, or shrink its shell?
  • Did first-load JavaScript grow, and was it a package, client boundary, barrel, or file?
  • Which routes share the same cause and source location?
  • What improved?
  • How much of the explanation is backed by attributed evidence?
  • What is the likely next action?
For one build, the stored evidence also answers:
  • Is this route static, partially static, ISR, dynamic, or a route handler?
  • Which dynamic API, uncached read, or route configuration produced that mode?
  • How much of the rendered route is in the static shell?
  • Which first-party modules and packages make up its first-load JavaScript?
  • Which client boundary or barrel import is responsible for that cost?
  • Which shared layout, provider, package, or call site affects the most routes?
  • How much of the build did crust classify, measure, and attribute?
  • Which three findings are most worth investigating first?
Every one of those questions is also answerable by the coding agent you already run — the same stored evidence, served over MCP with no model or API key. See Ask an agent.

The failure it exists to catch

Under Cache Components, removing a cache directive deep in a utility silently shrinks the static shell. No build error and no bundle growth:
The call site named there is three frames below the page, in a file the route never mentions.
Under the older experimental_ppr model a dynamic API outside a Suspense boundary threw a build error, so you found out immediately. Cache Components inverts the default: absence of caching is what postpones, and it fails quietly.

What it does

Compare two builds

Branches, tags, commits, and build ids with one decision, grouped causes, and improvements - building both refs on demand when neither has been measured.

Analyze a build

Prioritized findings, route modes, first-load JS, shell composition, and source-level reasons.

Understand regressions

What fails automatically, what needs a budget, and when crust refuses to compare.

Shell engine

Predicted versus actual static shell, and the call site that broke it.

Trace client JavaScript

Source-map attribution, client-boundary cost, barrel drag, and import chains.

Add CI

Zero-config regressions, optional ceilings, measured finding trust, and an updating PR comment.

Ask an agent

Serve stored evidence to the coding agent you already run - no model, embeddings, or API key.

What is enforced

With a comparable baseline, crust fails CI without a budget file when:
  • a rendering mode moves down the staticness scale
  • an uncached read appears where the baseline had none
  • a route that emitted a static shell stops emitting one
Project-specific ceilings remain explicit. Configure maximum first-load bytes, maximum percentage growth, and minimum shell ratios in .perf/budgets.json.
An added route is not a regression, unknown is never assigned a direction, and builds made with different bundlers, Next majors, or snapshot schemas are not compared. Absolute ceilings can still apply because they describe the current build rather than a delta.

What crust delivers

  • Production-build explanations connected to components, imports, and call sites
  • Cause-chain evidence, analysis coverage, shared-cause grouping, and actionable client cost
  • Configuration changes kept separate from application regressions
  • Compatible local history with route-level regression blame
  • Static-shell composition verified against emitted HTML
  • First-party client-JavaScript attribution
  • Evidence-backed merge decisions and explicit project budgets

Support

Next.js 15 and 16, webpack and Turbopack. Outside that range crust refuses to run rather than emit numbers it cannot stand behind. See support and non-goals.