---
title: "Bundle Analyzer"
description: "The Bundle Analyzer provides an interactive web-based interface and data pipeline for visualizing, traversing, and diffing bundle sizes and module compositions across Next.js application builds. By..."
last_updated: "2026-10-12T06:38:56.853737+00:00"
canonical_url: "https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/companion-packages/bundle-analyzer"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [apps/bundle-analyzer/lib/diff-treemap-layout.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/diff-treemap-layout.ts)
- [apps/bundle-analyzer/components/treemap-visualizer.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/treemap-visualizer.tsx)
- [apps/bundle-analyzer/components/analyzer.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/analyzer.tsx)
- [apps/bundle-analyzer/lib/treemap-layout.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/treemap-layout.ts)
- [apps/bundle-analyzer/components/diff-treemap.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/diff-treemap.tsx)
- [apps/bundle-analyzer/components/import-chain.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/import-chain.tsx)
- [apps/bundle-analyzer/lib/analyze-data.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/analyze-data.ts)
- [packages/next/src/build/analyze/graph-dump.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/analyze/graph-dump.ts)
- [apps/bundle-analyzer/components/sidebar.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/sidebar.tsx)
- [packages/next/src/cli/internal/static-routes-info.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/cli/internal/static-routes-info.ts)
- [apps/bundle-analyzer/app/compare/page.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/app/compare/page.tsx)
- [apps/bundle-analyzer/app/analyze/page.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/app/analyze/page.tsx)
- [apps/bundle-analyzer/app/page.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/app/page.tsx)
- [apps/bundle-analyzer/lib/module-graph.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/module-graph.ts)
- [apps/bundle-analyzer/lib/diff.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/diff.ts)
- [apps/bundle-analyzer/components/compare-layout.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/compare-layout.tsx)
- [packages/next/src/build/analyze/index.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/analyze/index.ts)
- [apps/bundle-analyzer/components/index.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/index.ts)
- [packages/next/src/cli/next-analyze.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/cli/next-analyze.ts)
- [apps/bundle-analyzer/app/layout.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/app/layout.tsx)
- [apps/bundle-analyzer/lib/analyzer-data.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/analyzer-data.ts)
- [packages/next/src/cli/internal/turbo-trace-server.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/cli/internal/turbo-trace-server.ts)
- [packages/next-bundle-analyzer/index.js](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-bundle-analyzer/index.js)
- [packages/next/analyze/graph-v1.schema.json](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/analyze/graph-v1.schema.json)
</details>

## Overview

The Bundle Analyzer provides an interactive web-based interface and data pipeline for visualizing, traversing, and diffing bundle sizes and module compositions across Next.js application builds. By parsing structured build artifacts and binary graph datasets emitted during production compilation, it empowers developers to inspect hierarchical directory structures, trace import dependencies, and pinpoint size regressions.
Sources: [apps/bundle-analyzer/app/layout.tsx:6-10](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/app/layout.tsx#L6-L10), [packages/next/src/build/analyze/graph-dump.ts:1-50](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/analyze/graph-dump.ts#L1-L50), [apps/bundle-analyzer/lib/analyze-data.ts:231-259](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/analyze-data.ts#L231-L259)

## Build Graph Ingestion and Schema

### CLI Export and Replay Pipeline

The analyzer pipeline initiates through the `next analyze` command or the export sub-pipeline managed by `nextAnalyzeExport`, which unpacks snapshot records without re-running compilation. The export flow reads history metadata from `diagnostics/analyze/history/history.json` and resolves target snapshots via `snapshotNameSchema` before invoking `dumpAnalyzeGraph` on the snapshot directory.
Sources: [packages/next/src/cli/next-analyze.ts:36-63](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/cli/next-analyze.ts#L36-L63)

| Option Name | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `experimentalAnalyze` | boolean | — | Enables experimental build graph analysis workflows. |
| `profile` | boolean | false | Activates CPU profiling and React production profiling during analysis. |
| `mangling` | boolean | true | Controls variable mangling; disabling logs a debugging warning. |
| `port` | number | 4000 | Port used by the interactive bundle analyzer web server. |
| `output` | boolean | false | Writes results to disk instead of starting the interactive server. |
| `experimentalAppOnly` | boolean | false | Restricts analysis strictly to the [App Router](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/client-runtime/app-router). |
| `snapshot` | string | — | Explicit identifier for capturing or loading a specific analysis snapshot. |

Sources: [packages/next/src/cli/next-analyze.ts:20-34](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/cli/next-analyze.ts#L20-L34)

> [!NOTE]
> Replaying snapshots via `nextAnalyzeExport` entirely bypasses `next.config` loading, lock acquisition, and fresh compilation artifacts, relying solely on previously serialized analysis records.
Sources: [packages/next/src/cli/next-analyze.ts:40-40](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/cli/next-analyze.ts#L40-L40)

### Binary Graph Data Structures and Schema Validation

Graph data is structured according to schema version `1` (`urn:nextjs:analyze:analyzer-graph:v1`), defining newline-delimited JSON record types: `meta`, `module`, `route`, `output`, `part`, and `group`. Binary files prepend a 4-byte big-endian JSON header length followed by the JSON payload and a packed binary edge section. Edges and offset indices are validated via `validateEdges`, ensuring record counts, offset monotonicity, and target index boundaries are strictly enforced.
Sources: [packages/next/src/build/analyze/graph-dump.ts:61-128](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/analyze/graph-dump.ts#L61-L128), [packages/next/analyze/graph-v1.schema.json:3-13](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/analyze/graph-v1.schema.json#L3-L13)

```mermaid
flowchart TD
    A["Raw Artifact File"] --> B["Read 4-byte BE Length"]
    B --> C["Parse JSON Header"]
    C --> D["Verify schema_version === 1"]
    D --> E["Extract Binary Subarray"]
    E --> F["validateEdges() (Offsets & Targets)"]
    F --> G["Ingested Graph Data"]
```
Sources: [packages/next/src/build/analyze/graph-dump.ts:61-128](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/analyze/graph-dump.ts#L61-L128)

> [!WARNING]
> `module_index_hash` acts as an ordered, snapshot-scoped fingerprint of `modules.data` indices; it guarantees matching snapshot structures but does not serve as proof of identical graphs or build outputs.
Sources: [packages/next/analyze/graph-v1.schema.json:46-49](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/analyze/graph-v1.schema.json#L46-L49)

> [!CAUTION]
> If `header.schema_version` differs from `1`, `readData` throws an explicit validation error rather than attempting schema migration or fallback parsing.
Sources: [packages/next/src/build/analyze/graph-dump.ts:72-76](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/analyze/graph-dump.ts#L72-L76)

## Client-Side Data Fetching and Models

### SWR Data Hydration and Snapshot Resolution

The client bundle analyzer manages state via custom hooks built on top of `useSWR`. The `useAnalyzerModel` hook fetches history metadata, active route configurations, route summaries, and raw binary analyzer payloads. Static endpoints such as `/data/metadata.json`, `${comparisonBaseDir}/routes.json`, and `${comparisonBaseDir}/route-summaries.json` are retrieved with `revalidateOnFocus` and `revalidateOnReconnect` explicitly set to `false`. Dynamic data files including `${comparisonBaseDir}/modules.data` and `analyze.data` are loaded with `suspense: true` enabled to coordinate asynchronous asset unpacking.
Sources: [apps/bundle-analyzer/components/analyzer.tsx:122-130](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/analyzer.tsx#L122-L130), [apps/bundle-analyzer/components/analyzer.tsx:165-198](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/analyzer.tsx#L165-L198), [apps/bundle-analyzer/components/analyzer.tsx:313-316](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/analyzer.tsx#L313-L316), [apps/bundle-analyzer/lib/analyzer-data.ts:6-14](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/analyzer-data.ts#L6-L14)

| Hook Name | Default SWR Options | Suspense | Purpose |
| :--- | :--- | :--- | :--- |
| `useHistoryIndex` | `revalidateOnFocus: false`, `revalidateOnReconnect: false`, `shouldRetryOnError: false` | `false` | Fetches history index data from `/history/history.json`. |
| `useSuspenseJsonData` | `revalidateOnFocus: false`, `revalidateOnReconnect: false` (overridable) | `true` | Generic helper for fetching JSON assets with React Suspense enabled. |
| `useSWR` (`modulesData`) | None specified | `true` | Fetches binary module index data from `${comparisonBaseDir}/modules.data`. |
| `useSWR` (`analyzeData`) | `revalidateOnFocus: false`, `revalidateOnReconnect: false` | `true` | Fetches and parses binary route-specific data buffers into `AnalyzeData` instances. |

Sources: [apps/bundle-analyzer/components/analyzer.tsx:122-125](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/analyzer.tsx#L122-L125), [apps/bundle-analyzer/components/analyzer.tsx:165-191](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/analyzer.tsx#L165-L191), [apps/bundle-analyzer/components/analyzer.tsx:172-175](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/analyzer.tsx#L172-L175), [apps/bundle-analyzer/lib/analyzer-data.ts:6-33](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/analyzer-data.ts#L6-L33)

> [!NOTE]
> `useAnalyzerModel` invokes `useSearchParams()` before rendering suspense boundaries, allowing static prerendering bailouts to execute correctly prior to asynchronous data resolution.
Sources: [apps/bundle-analyzer/components/analyzer.tsx:106-108](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/analyzer.tsx#L106-L108)

### Module Graph Model Generation and Scope Classification

Once module and analysis buffers are fetched, `AnalyzeData` parses a 4-byte big-endian JSON header length, decodes the UTF-8 JSON header metadata, and wraps the trailing binary buffer in a `DataView`. Module relationships are processed by `computeActiveEntries`, which scans modules matching known Next.js template identifiers such as `next/dist/esm/build/templates/pages.js`, `next/dist/esm/build/templates/app-page.js`, and `[next]/entry/page-loader.ts`.
Sources: [apps/bundle-analyzer/lib/analyze-data.ts:236-252](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/analyze-data.ts#L236-L252), [apps/bundle-analyzer/lib/module-graph.ts:25-79](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/module-graph.ts#L25-L79)

```typescript
const potentialEntryDependents = [
  'next/dist/esm/build/templates/pages.js',
  'next/dist/esm/build/templates/pages-api.js',
  'next/dist/esm/build/templates/pages-edge-api.js',
  'next/dist/esm/build/templates/edge-ssr.js',
  'next/dist/esm/build/templates/app-route.js',
  'next/dist/esm/build/templates/edge-app-route.js',
  'next/dist/esm/build/templates/app-page.js',
  'next/dist/esm/build/templates/edge-ssr-app.js',
  'next/dist/esm/build/templates/middleware.js',
  '[next]/entry/page-loader.ts',
]
```
Sources: [apps/bundle-analyzer/lib/module-graph.ts:29-40](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/module-graph.ts#L29-L40)

Module depth maps are built via breadth-first search utilizing `computeModuleDepthMap`. Async dependencies impose a depth penalty of `1000` and are buffered into a delayed queue sorted in descending order of depth to guarantee correct hierarchical evaluation order. Finally, `computeSourceLoadScopes` classifies every source index into one of five load scopes: `'initial'`, `'async'`, `'mixed'`, `'traced'`, or `'unknown'`.
Sources: [apps/bundle-analyzer/lib/module-graph.ts:86-167](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/module-graph.ts#L86-L167), [apps/bundle-analyzer/lib/module-graph.ts:173-244](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/module-graph.ts#L173-L244)

| Scope Name | Bitmask Value | Description |
| :--- | :--- | :--- |
| `ModuleLoadScope.Initial` | `1` | Modules loaded immediately during initial route entry. |
| `ModuleLoadScope.Async` | `2` | Modules loaded via asynchronous chunks or dynamic imports. |
| `ModuleLoadScope.Traced` | `4` | Modules discovered via dependency graph tracing heuristics. |

Sources: [apps/bundle-analyzer/lib/module-graph.ts:3-7](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/module-graph.ts#L3-L7)

> [!CAUTION]
> `assertMatchingModuleIndex` verifies that the `module_index_hash` string from `ModulesData` matches the fingerprint inside `AnalyzeData`, throwing an explicit error if a route record is paired with a mismatched module table.
Sources: [apps/bundle-analyzer/lib/analyze-data.ts:215-226](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/analyze-data.ts#L215-L226)

## Treemap Layout Calculation

### Hierarchical Directory Packing and Metadata Precomputation

The treemap layout engine processes hierarchical module data by first executing a bottom-up precomputation sweep across all sources via `precomputeSourceMetadata`. This pass calculates aggregate uncompressed and compressed sizes for every directory node and establishes whether a source is filtered out based on visibility rules.
Sources: [apps/bundle-analyzer/lib/treemap-layout.ts:83-142](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/treemap-layout.ts#L83-L142)

```typescript
export enum SizeMode {
  Compressed = 'compressed',
  Uncompressed = 'uncompressed',
}
```
Sources: [apps/bundle-analyzer/lib/treemap-layout.ts:42-45](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/treemap-layout.ts#L42-L45)

During recursive layout generation inside `computeTreemapLayoutFromAnalyzeInternal`, single-child directories are folded together when their combined path length does not exceed `40` characters, reducing visual clutter in deep folder structures.
Sources: [apps/bundle-analyzer/lib/treemap-layout.ts:162-180](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/treemap-layout.ts#L162-L180)

> [!NOTE]
I> Directory nodes whose height drops below `30` pixels automatically switch to a collapsed state (`'collapsed-directory'`), bypassing child coordinate computation and rendering an aggregate item count instead.
Sources: [apps/bundle-analyzer/lib/treemap-layout.ts:206-237](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/treemap-layout.ts#L206-L237)

### Descendant Counting and Coordinate Computation

When a directory node is collapsed due to spatial constraints, it recursively counts all descendant files using a depth-first traversal of the child indices.
Sources: [apps/bundle-analyzer/lib/treemap-layout.ts:216-224](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/treemap-layout.ts#L216-L224)

```typescript
// Count all descendant files
function countDescendants(idx: SourceIndex): number {
  const children = analyzeData.sourceChildren(idx)
  if (children.length === 0) return 1
  return children.reduce(
    (sum, childIdx) => sum + countDescendants(childIdx),
    0
  )
}
```
Sources: [apps/bundle-analyzer/lib/treemap-layout.ts:216-224](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/treemap-layout.ts#L216-L224)

For uncollapsed directories with visible children, child nodes are filtered, mapped to their active size weights under either `SizeMode.Compressed` or `SizeMode.Uncompressed`, and sorted in descending order of size before invoking `layoutTreemap` to compute bounding rectangles within the content area.
Sources: [apps/bundle-analyzer/lib/treemap-layout.ts:240-282](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/treemap-layout.ts#L240-L282)

| SizeMode Constant | Value String | Description |
| :--- | :--- | :--- |
| `SizeMode.Compressed` | `'compressed'` | Uses gzip/brotli compressed size for node sizing and weights. |
| `SizeMode.Uncompressed` | `'uncompressed'` | Uses raw uncompressed byte size for node sizing and weights. |

Sources: [apps/bundle-analyzer/lib/treemap-layout.ts:42-45](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/treemap-layout.ts#L42-L45)

## Interactive Treemap Canvas Rendering

### Canvas Drawing Loop and Visual Hierarchy Styling

```tsx
function getFileColor(
  node: {
    js?: boolean
    css?: boolean
    json?: boolean
    asset?: boolean
    server?: boolean
    client?: boolean
    traced?: boolean
    specialModuleType: SpecialModule | null
  },
  colors: CanvasColors
): string {
  const { js, css, json, asset, client, traced, specialModuleType } = node

  if (isPolyfill(specialModuleType)) {
    return colors.polyfill
  }

  let color = '#9ca3af' // gray-400 default
  if (js) color = '#4682b4'
  if (css) color = '#8b7d9e'
  if (json) color = '#297a3a'
  if (asset) color = '#da2f35'

  if (!client) {
    // Make it darker for server (30% darker)
    color = darken(0.3, color)

    if (traced) {
      // Make it slightly lighter (15% lighter than darkened)
      color = lighten(0.15, color)
    }
  }
  return color
}
```
Sources: [apps/bundle-analyzer/components/treemap-visualizer.tsx:57-92](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/treemap-visualizer.tsx#L57-L92)

File tile coloring relies on file type flags and special module classifiers. Polyfill modules receive the dedicated polyfill color, while standard files are mapped using base hex values: `#4682b4` for JavaScript (`js`), `#8b7d9e` for CSS (`css`), `#297a3a` for JSON (`json`), and `#da2f35` for assets (`asset`), defaulting to `#9ca3af`. Non-client (`server`) modules apply a `0.3` darkening factor via `darken(0.3, color)`, and if traced (`traced`), are lightened by `0.15` using `lighten(0.15, color)`.
Sources: [apps/bundle-analyzer/components/treemap-visualizer.tsx:57-92](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/treemap-visualizer.tsx#L57-L92)

| File Type / Flag | Base Hex Color / Transformation | Description |
| :--- | :--- | :--- |
| Polyfill Module | `colors.polyfill` | Applied when `isPolyfill()` matches special module types. |
| JavaScript (`js`) | `#4682b4` | Default color for client/server JS files. |
| CSS (`css`) | `#8b7d9e` | Stylesheet bundles. |
| JSON (`json`) | `#297a3a` | JSON data modules. |
| Asset (`asset`) | `#da2f35` | Static assets and binary files. |
| Server Non-Client | `darken(0.3, color)` | Darkens base module color by 30% for server environments. |
| Traced Server | `lighten(0.15, darkenedColor)` | Lightens darkened server color by 15% when traced. |

Sources: [apps/bundle-analyzer/components/treemap-visualizer.tsx:57-92](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/treemap-visualizer.tsx#L57-L92)

Title and size font sizing inside rendered nodes is derived dynamically from the allocated title bar height using `calculateTitleFontSizes`. The title font size is restricted to a maximum of `10` pixels or half the title bar height (`titleBarHeight * 0.5`), while the size font takes the minimum of `9` pixels or `titleFontSize - 2`. Text measurement uses an LRU-style cache bounded at `30,000` entries (`TEXT_WIDTH_CACHE_SIZE`) combined with `measureTextCached` to prevent layout performance degradation.
Sources: [apps/bundle-analyzer/components/treemap-visualizer.tsx:101-138](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/treemap-visualizer.tsx#L101-L138)

> [!NOTE]
> `truncateTextWithEllipsisIfNeeded` computes text fitting using binary search over the string length to insert ellipsis markers (`...`) without repeatedly polluting the text width cache with failed measurement attempts.
Sources: [apps/bundle-analyzer/components/treemap-visualizer.tsx:140-170](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/treemap-visualizer.tsx#L140-L170)

### Hit Testing and Zooming Mechanics

```tsx
function findNodeAtPosition(
  node: LayoutNode,
  x: number,
  y: number
): LayoutNode | null {
  const { rect } = node

  // Check if point is within this node's bounds
  if (
    x < rect.x ||
    x > rect.x + rect.width ||
    y < rect.y ||
    y > rect.y + rect.height
  ) {
    return null
  }

  if (node.type === 'collapsed-directory') {
    return node
  }

  // For regular directories, check if we're in the title bar first
  if (node.type === 'directory') {
    const titleBarHeight = node.titleBarHeight || 0
    if (y >= rect.y && y <= rect.y + titleBarHeight) {
      return node // Clicked on title bar
    }
  }

  // Check children (if any)
  if (node.children) {
    for (const child of node.children) {
      const found = findNodeAtPosition(child, x, y)
      if (found) return found
    }
  }

  return node
}
```
Sources: [apps/bundle-analyzer/components/treemap-visualizer.tsx:172-210](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/treemap-visualizer.tsx#L172-L210)

Hit testing is executed by `findNodeAtPosition`, which recursively traverses layout nodes to locate the deepest matching rectangle enclosing coordinates `(x, y)`. If a node is a collapsed directory (`'collapsed-directory'`), it returns immediately. For standard directory nodes, it checks whether the vertical coordinate falls within the title bar bounds (`y >= rect.y && y <= rect.y + titleBarHeight`), returning the directory node itself when matched; otherwise, it descends into child nodes.
Sources: [apps/bundle-analyzer/components/treemap-visualizer.tsx:172-210](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/treemap-visualizer.tsx#L172-L210)

> [!WARNING]
> Pressing the `Escape` key globally dispatches a window keydown event handler in `AnalyzerModel` that resets both the selected source index and focused source index back to the root source index of the active analysis data, provided an input or textarea element is not actively focused.
Sources: [apps/bundle-analyzer/components/analyzer.tsx:223-244](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/analyzer.tsx#L223-L244)

## Import Chain Dependency Traversal

### Sidebar Component and Selection Details

When a source file is selected in the bundle analyzer, the `Sidebar` component delegates rendering to `SelectionDetails`, which inspects whether the node contains child modules using `analyzeData.sourceChildren(selectedSourceIndex).length > 0`. If a node is a leaf module lacking child nodes, `SelectionDetails` renders the `ImportChain` component alongside output chunk identifiers extracted via `analyzeData.sourceChunks(selectedSourceIndex)`. 

```tsx
{selectedSource && !hasChildModules && (
  <>
    <ImportChain
      startFileId={selectedSourceIndex}
      analyzeData={analyzeData}
      modulesData={modulesData}
      depthMap={moduleDepthMap}
      environmentFilter={environmentFilter}
    />
    {chunks.length > 0 ? (
      <div className="mt-2">
        <p className="text-xs font-semibold text-foreground">
          Output Chunks
        </p>
        <ul className="text-xs text-muted-foreground font-mono mt-1 space-y-1">
          {chunks.map((chunk) => (
            <li key={chunk} className="break-all">
              {chunk}
            </li>
          ))}
        </ul>
      </div>
    ) : null}
  </>
)}
```
Sources: [apps/bundle-analyzer/components/sidebar.tsx:157-181](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/sidebar.tsx#L157-L181)

The `ImportChain` component manages route filtering through a checkbox bound to `currentRouteOnly` state, allowing users to toggle between including only dependent modules tied to the current route's bundle or inspecting all routes. State tracking for selected dependents across chain levels is maintained using `selectedIndices`. Handlers `handlePrevious` and `handleNext` update these indices by decrementing or incrementing with bounds-checking against `level.totalCount - 1`, slicing the index array back to `levelIndex + 1`.

```tsx
export function ImportChain({
  startFileId,
  analyzeData,
  modulesData,
  depthMap,
  environmentFilter,
}: ImportChainProps) {
  // Filter to include only the current route
  const [currentRouteOnly, setCurrentRouteOnly] = useState(true)

  // Track which dependent is selected at each level
  const [selectedIndices, setSelectedIndices] = useState<number[]>([])

  const chain = buildImportChain(
    startFileId,
    analyzeData,
    modulesData,
    depthMap,
    environmentFilter,
    selectedIndices,
    currentRouteOnly
  )

  const handlePrevious = (levelIndex: number) => {
    setSelectedIndices((prev) => {
      const newIndices = [...prev]
      const currentIdx = newIndices[levelIndex] ?? 0
      const level = chain[levelIndex]
      newIndices[levelIndex] =
        currentIdx > 0 ? currentIdx - 1 : level.totalCount - 1
      return newIndices.slice(0, levelIndex + 1)
    })
  }

  const handleNext = (levelIndex: number) => {
    setSelectedIndices((prev) => {
      const newIndices = [...prev]
      const currentIdx = newIndices[levelIndex] ?? 0
      const level = chain[levelIndex]
      newIndices[levelIndex] =
        currentIdx < level.totalCount - 1 ? currentIdx + 1 : 0
      return newIndices.slice(0, levelIndex + 1)
    })
  }
```
Sources: [apps/bundle-analyzer/components/import-chain.tsx:311-354](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/import-chain.tsx#L311-L354)

### Layer Icons and Graph Traversal

Each level of the rendered import chain evaluates module layers against regular expressions to determine badge icon states. The layer types and corresponding icon mapping rules are outlined below:

| Layer Matcher | Icon Component | Icon Color / State | Description |
| :--- | :--- | :--- | :--- |
| `/client/` | `PanelTop` | Green (`text-green-500`) if `flags.client`, else Gray | Included on client-side of this route |
| `/ssr/` | `Globe` | Blue (`text-blue-500`) if `flags.server`, else Gray | Included on server-side rendering layer |
| `/rsc/` | `Server` | Orange (`text-orange-500`) if `flags.server`, else Gray | Included as Server Component |
| `/route\|api/` | `SquareFunction` | Red (`text-red-500`) if `flags.server`, else Gray | Included for API routes |
| `app` / `pages` / `externals-tracing` | `Box` / `File` / `Package` | Layer-specific colors | Fallback handler via `LayerIcon` for App/Pages routers and external assets |

Sources: [apps/bundle-analyzer/components/import-chain.tsx:478-580](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/import-chain.tsx#L478-L580)

> [!WARNING]
> `buildImportChain` incorporates a hard safety limit that breaks out of dependent traversal loops if `levelIndex > 100`, preventing infinite recursion when circular dependencies exist in the module graph.
Sources: [apps/bundle-analyzer/components/import-chain.tsx:304-306](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/import-chain.tsx#L304-L306)

## Bundle Comparison and Diffing

### Synthetic Diff Layout Generation

The bundle analyzer computes build-to-build size differentials by joining leaf source entries across two `AnalyzeData` datasets using their stable full source paths. The `diffSources` function filters out directory aggregations and empty contributions where both size and compressed size evaluate to zero, collecting per-side leaf entries into maps keyed by file path. Each resulting diff row captures status flags, absolute sizes, compressed sizes, and OR-merged environment flags across both sides.

```typescript
function collectLeaves(
  side: 'A' | 'B',
  data: AnalyzeData,
  options: SourceDiffOptions
): Map<string, LeafEntry> {
  const out = new Map<string, LeafEntry>()
  const count = data.sourceCount()
  for (let i = 0; i < count; i++) {
    // Only include leaf sources (real files, not directory aggregations).
    if (data.sourceChildren(i).length > 0) continue
    if (options.filterSource && !options.filterSource(side, i)) continue
    const fullPath = data.getFullSourcePath(i)
    if (!fullPath) continue
    const own = data.getOwnSizes(i)
    // Skip empty contributions — they aren't really "in" the bundle.
    if (own.size === 0 && own.compressedSize === 0) continue
    const flags = data.getSourceFlags(i)
    out.set(fullPath, {
      index: i,
      size: own.size,
      compressedSize: own.compressedSize,
      client: flags.client,
      server: flags.server,
    })
  }
  return out
}

/**
 * Diffs every source between two analyze datasets, joining by full source
 * path (stable across builds modulo file moves).
 *
 * Directories (sources with children) are excluded — only leaf "files" appear
 * in the resulting rows. Their own size is the sum of their {@link
 * AnalyzeData.sourceChunkParts} contributions.
 */
export function diffSources(
  analyzeA: AnalyzeData | null,
  analyzeB: AnalyzeData | null,
  options: SourceDiffOptions = {}
): DiffSummary<SourceDiffRow> {
  const summary = emptySummary<SourceDiffRow>()

  const leavesA = analyzeA
    ? collectLeaves('A', analyzeA, options)
    : new Map<string, LeafEntry>()
  const leavesB = analyzeB
    ? collectLeaves('B', analyzeB, options)
    : new Map<string, LeafEntry>()

  const allKeys = new Set<string>([...leavesA.keys(), ...leavesB.keys()])
  for (const key of allKeys) {
    const a = leavesA.get(key)
    const b = leavesB.get(key)
    const pretty = prettifySourcePath(key)
    pushRow(summary, {
      key,
      name: pretty.display,
      status: statusFor(
        a?.size ?? 0,
        b?.size ?? 0,
        a !== undefined,
        b !== undefined
      ),
      sizeA: a?.size ?? 0,
      sizeB: b?.size ?? 0,
      compressedA: a?.compressedSize ?? 0,
      compressedB: b?.compressedSize ?? 0,
      sourceIndexA: a?.index ?? null,
      sourceIndexB: b?.index ?? null,
      pathKind: pretty.kind,
      packageName: pretty.packageName,
      // OR-merge env flags across both sides so the badge reflects the
      // row's overall environment(s). A source that's client-only in A
      // and removed in B is still "client" for the purpose of the row.
      client: (a?.client ?? false) || (b?.client ?? false),
      server: (a?.server ?? false) || (b?.server ?? false),
    })
  }

```
Sources: [apps/bundle-analyzer/lib/diff.ts:237-318](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/diff.ts#L237-L318)

To visualize these differentials within the treemap interface, `createDiffTreemapLayout` constructs a synthetic hierarchical tree containing only changed modules. Path segments are parsed from row keys—treating Next.js project output paths starting with `[project]/.next/` as unattributed output basenames—and nested directory nodes are instantiated dynamically. 

```typescript
export function createDiffTreemapLayout(
  rows: SourceDiffRow[],
  useCompressed: boolean
): DiffTreemapLayout {
  let nextIndex = 0
  const root: DiffTreeNode = {
    index: nextIndex++,
    name: 'All Changed Modules',
    parentIndex: null,
    size: 0,
    children: [],
  }
  const nodeByIndex = new Map<number, DiffTreeNode>([[root.index, root]])
  const directoryByPath = new Map<string, DiffTreeNode>([['', root]])
  const rowBySourceIndex = new Map<number, SourceDiffRow>()
  const sourceIndexByKey = new Map<string, number>()

  for (const row of rows) {
    const change = delta(row, useCompressed)
    if (change === 0) continue

    const parts = isNextBuildOutput(row.key)
      ? ['Unattributed output', getBasename(row.key)]
      : row.key.split('/').filter(Boolean)
    let parent = root
    let directoryPath = ''

    for (const part of parts.slice(0, -1)) {
      directoryPath += `/${part}`
      let directory = directoryByPath.get(directoryPath)
      if (!directory) {
        directory = {
          index: nextIndex++,
          name: `${part}/`,
          parentIndex: parent.index,
          size: 0,
          children: [],
        }
        parent.children.push(directory)
        directoryByPath.set(directoryPath, directory)
        nodeByIndex.set(directory.index, directory)
      }
      parent = directory
    }

    const leaf: DiffTreeNode = {
      index: nextIndex++,
      name: parts.at(-1) ?? row.name,
      parentIndex: parent.index,
      size: Math.abs(change),
      row,
      children: [],
    }
    parent.children.push(leaf)
    nodeByIndex.set(leaf.index, leaf)
    rowBySourceIndex.set(leaf.index, row)
    sourceIndexByKey.set(row.key, leaf.index)
  }
```
Sources: [apps/bundle-analyzer/lib/diff-treemap-layout.ts:20-77](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/diff-treemap-layout.ts#L20-L77)

> [!NOTE]
> The absolute magnitude of the change (`Math.abs(change)`) is assigned as the leaf node size, ensuring that tiles in the diff treemap directly represent size deltas rather than absolute file footprints.
Sources: [apps/bundle-analyzer/lib/diff-treemap-layout.ts:65-72](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/diff-treemap-layout.ts#L65-L72)

### Layout Calculations and Color Mapping

Once leaf and directory node sizes are aggregated recursively via `aggregateSize`, `computeLayout` processes rectangular bounds by determining title bar heights and checking collapse states. If a directory rectangle's height drops below 30 pixels, it is rendered as a collapsed directory containing an item count; otherwise, child nodes are sorted descending by size and passed to the treemap layout engine.

```typescript
function aggregateSize(node: DiffTreeNode): number {
  if (node.row) return node.size
  node.size = node.children.reduce(
    (total, child) => total + aggregateSize(child),
    0
  )
  return node.size
}
aggregateSize(root)

function computeLayout(node: DiffTreeNode, rect: LayoutRect): LayoutNode {
  if (node.row) {
    return {
      name: node.name,
      size: node.size,
      type: 'file',
      rect,
      sourceIndex: node.index,
      specialModuleType: null,
      client: node.row.client,
      server: node.row.server,
    }
  }

  const titleBarHeight = Math.round(
    Math.max(12, Math.min(24, rect.height * 0.1))
  )
  const isCollapsed = rect.height < 30
  if (isCollapsed) {
    return {
      name: node.name,
      size: node.size,
      type: 'collapsed-directory',
      rect,
      titleBarHeight,
      children: [],
      itemCount: countLeaves(node),
      sourceIndex: node.index,
      specialModuleType: null,
    }
  }

  const contentRect: LayoutRect = {
    x: Math.round(rect.x),
    y: Math.round(rect.y + titleBarHeight),
    width: Math.max(0, Math.round(rect.width - 2)),
    height: Math.max(0, Math.round(rect.height - titleBarHeight - 2)),
  }
  const children = [...node.children].sort((a, b) => b.size - a.size)
  const childRects = layoutTreemap(
    children.map((child) => child.size),
    contentRect
  )

  return {
    name: node.name,
    size: node.size,
    type: 'directory',
    rect,
    titleBarHeight,
    children: children.map((child, index) =>
      computeLayout(child, childRects[index])
    ),
    sourceIndex: node.index,
    specialModuleType: null,
  }
}
```
Sources: [apps/bundle-analyzer/lib/diff-treemap-layout.ts:79-145](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/lib/diff-treemap-layout.ts#L79-L145)

The `DiffTreemap` component overrides file colors and size labels using `colorForRow`, scaling fill opacities based on the relative magnitude of the change against baseline measurements.

```tsx
  const diffLayout = useMemo(
    () => createDiffTreemapLayout(summary.rows, useCompressed),
    [summary.rows, useCompressed]
  )
  const { rowBySourceIndex, sourceIndexByKey } = diffLayout

  function getFileColorOverride(
    node: LayoutNode,
    colors: CanvasColors
  ): string | undefined {
    if (node.sourceIndex === undefined) return undefined
    const row = rowBySourceIndex.get(node.sourceIndex)
    return row ? colorForRow(row, useCompressed, colors) : undefined
  }

  // Show size deltas on changed tiles instead of absolute sizes.
  function getFileSizeLabel(node: LayoutNode): string | undefined {
    if (node.sourceIndex === undefined) return undefined
    const row = rowBySourceIndex.get(node.sourceIndex)
    if (!row || row.status === 'identical') return undefined
    return formatDelta(delta(row, useCompressed))
  }

  // Focus state stays local: it controls drill-in/zoom, which is purely a
  // visual concern of the treemap and shouldn't affect the sidebar.
  const initialRoot = diffLayout.rootIndex
  const [focusedSourceIndex, setFocusedSourceIndex] =
    useState<number>(initialRoot)

  // Translate selection into the synthetic tree's index. If the selected
  // path has no size delta, fall back to the root of changed files.
  const selectedSourceIndex =
    (selectedKey != null ? sourceIndexByKey.get(selectedKey) : undefined) ??
    initialRoot

  const handleSelectSourceIndex = (idx: number) => {
    if (!onSelectKey) return
    const row = rowBySourceIndex.get(idx)
    onSelectKey(row?.key ?? null)
  }

  if (!data) {
    return (
      <div className="flex h-full items-center justify-center text-sm text-muted-foreground">
        No data for this route.
      </div>
    )
  }

  return (
    <TreemapVisualizer
      source={diffLayout}
      selectedSourceIndex={selectedSourceIndex}
      onSelectSourceIndex={handleSelectSourceIndex}
      focusedSourceIndex={focusedSourceIndex}
      onFocusSourceIndex={setFocusedSourceIndex}
      getFileSizeLabel={getFileSizeLabel}
      sizeMode={useCompressed ? SizeMode.Compressed : SizeMode.Uncompressed}
      getFileColorOverride={getFileColorOverride}
      searchQuery={searchQuery}
      overlay={<DiffLegend />}
    />
  )
}

/**
 * Picks a fill color for a diff row. Mirrors the previous DiffTreemap's
 * scheme: bright blue/amber for added/removed and lighter tints scaled by the
 * relative magnitude of the change for grew/shrank.
 */
function colorForRow(
  row: SourceDiffRow,
  useCompressed: boolean,
  colors: CanvasColors
): string {
  if (row.status === 'added') return colors.increase
  if (row.status === 'removed') return colors.decrease
  const deltaValue = useCompressed
    ? row.compressedB - row.compressedA
    : row.sizeB - row.sizeA
```
Sources: [apps/bundle-analyzer/components/diff-treemap.tsx:63-150](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/apps/bundle-analyzer/components/diff-treemap.tsx#L63-L150)

## Related

- [Build Pipeline](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/build-system/build-pipeline)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/llms.txt) for all pages in this wiki.
