Skip to main content

Route to chunk

The two bundlers expose this in opposite ways, which is why crust keeps one adapter interface and two implementations rather than branching inline. On webpack, the client reference manifest holds the whole app’s client module table - the manifest emitted for /dashboard lists chunks belonging to /. Unioning it would attribute every client component to every route, so crust intersects it against the route’s own convention chunk instead.
app-build-manifest.json does not exist in Next 16, and build-manifest.json carries no App Router route data at all. It is still present in Next 15 - the manifest set genuinely differs across the two supported majors.

Chunk to source

Byte ranges are mapped through source maps. Each mapping owns the bytes from its own position to the next; regions no mapping covers are counted as unattributed rather than smeared onto the nearest source. Measured on the fixture apps: webpack’s shortfall is almost entirely one file - polyfills-*.js ships with no map. Excluding it, webpack reaches ~99.6%.
Turbopack names source maps independently of their chunk (3cqmf8g-py4nf.js → 3ixj0_2my9s3k.js.map). Guessing chunk + '.map' - webpack’s actual convention - finds nothing and reports 17.5%. crust follows the sourceMappingURL comment.

Identifying your code

Deciding whether a mapped source is yours is not a prefix check. Next ships source maps that point into its own src/, so a !path.includes('node_modules') test reports Next’s router and segment cache as your application code. The bundlers also disagree on anchoring: webpack emits webpack://_N_E/./components/x.tsx, Turbopack emits turbopack:///[project]/… where [project] is the inferred workspace root, not the app directory. crust resolves a source by asking whether it names a file that actually exists in the workspace, matching in both directions - the source path can be shorter or longer than the indexed path. Ambiguity resolves to nothing, never to a guess.

Buckets

Every byte lands in exactly one of:
  • your modules - resolved to a workspace file
  • dependencies - resolved to a package name, with the pnpm virtual store segment skipped
  • outside project - mapped, but to a path carrying no package name (Next’s own ../../src/…)
  • unattributed - no mapping covers it, or the chunk shipped without a map
The last bucket is reported, not hidden. Knowing how much cannot be explained is the point.

Route to source line

Byte attribution answers what shipped. Cause chains answer why the route reached it. crust preserves binding names while walking components, imports, and first-party calls:
Direct imports, aliases, default exports, named exports, and barrel re-exports remain explicit hops. If a unique path cannot be proven, the chain stops at the unresolved segment. Conclusions are labelled verified, inferred, or unknown, while separate coverage counts show classified routes, measured shells, attributed bytes, and conservative fallbacks.

Client cost and shared causes

Client boundaries are charged for their reachable subtree. Barrel cost includes only modules that become unreachable when that barrel is removed. Shared layouts, boundaries, packages, barrels, chunks, and call sites are grouped once with every affected route, so one root cause does not become twenty repeated findings. Across builds, crust diff compares three byte-attribution axes directly:
  • packages - dependency movement grouped over every affected route
  • client boundaries - movement in the attributed subtree crossing into the client
  • barrels - movement in modules that reach a route only through the barrel import
These axes can overlap: a client boundary may contain a package reached through a barrel. crust does not add all three together and pretend the same bytes were explained multiple times. It keeps the largest overlapping explanation, reports attribution coverage beside the verdict, and preserves route detail when the evidence does not fully explain the movement.