Architecture Overview
Server Runtime
Rendering Pipeline
Client Navigation
Caching and Export
Development Tools
Build and Configuration
Ecosystem Packages
Testing Infrastructure
How It Works
The following files were used as context for generating this wiki page:
The Bundle Analyzer provides an interactive web application and command-line tool suite for inspecting, visualizing, and auditing Next.js application bundle compositions. By parsing structured binary payloads and module relationship graphs generated during builds, the analyzer solves the complexity of understanding heavyweight compiled outputs, large node_modules footprints, and unoptimized chunk distribution. It embodies design decisions centered around binary data views for compact storage, performant client-side treemap layout calculations, and deep dependency chain traversal. The analyzer works in tandem with build-time diagnostics and the Next.js CLI infrastructure to offer developers immediate visual insights into client and server bundle sizing.
Sources: apps/bundle-analyzer/app/page.tsx:1-283, apps/bundle-analyzer/lib/analyze-data.ts:208-236, packages/next/src/cli/next-analyze.ts:21-59
The bundle analysis infrastructure is exposed through two primary entry points: the @next/bundle-analyzer public configuration wrapper and the next-analyze CLI command. The configuration wrapper acts as a high-order function accepting options such as enabled, openAnalyzer, analyzerMode, and logLevel, returning a function that transforms a NextConfig object.
When invoked, the wrapper checks whether enabled is truthy and whether process.env.TURBOPACK is set. If Turbopack is active, a warning is emitted instructing developers to use next experimental-analyze or pass --webpack to next build, bypassing further modifications. Otherwise, it configures reportFilename based on options.nextRuntime and injects the BundleAnalyzerPlugin from webpack-bundle-analyzer into the Webpack configuration.
module.exports =
({ enabled = true, logLevel, openAnalyzer, analyzerMode } = {}) =>
(nextConfig = {}) => {
if (!enabled) {
return nextConfig
}
if (process.env.TURBOPACK) {
console.warn(
'The Next Bundle Analyzer is not compatible with Turbopack builds, no report will be generated.\n\n' +
'Consider trying the new Turbopack analyzer via `next experimental-analyze`.\n\n' +
'See https://nextjs.org/docs/app/guides/package-bundling for more information\n\n' +
'To run this analysis pass the `--webpack` flag to `next build`'
)
return nextConfig
}
const extension = analyzerMode === 'json' ? '.json' : '.html'
return Object.assign({}, nextConfig, {
webpack(config, options) {
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
config.plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: analyzerMode || 'static',
logLevel,
openAnalyzer,
reportFilename: !options.nextRuntime
? `./analyze/client${extension}`
: `../${options.nextRuntime === 'nodejs' ? '../' : ''}analyze/${
options.nextRuntime
}${extension}`,
})
)
if (typeof nextConfig.webpack === 'function') {
return nextConfig.webpack(config, options)
}
return config
},
})
}The next-analyze CLI command manages signal handlers (SIGTERM and SIGINT) to save CPU profiles before exiting, validates project directory existence via getProjectDir(), and delegates execution to the internal analyze build runner.
Warning
The @next/bundle-analyzer plugin explicitly aborts report generation and logs a warning if process.env.TURBOPACK is detected, because it relies on Webpack's plugin architecture and webpack-bundle-analyzer. Turbopack users must use next experimental-analyze instead.
The bundle analyzer relies on a binary-backed schema that directly mirrors Rust data models (analyze.rs), parsing a hybrid payload consisting of a variable-length JSON header followed by raw binary edge and dependency structures. The AnalyzeData class coordinates parsing of this buffer, extracting structure definitions for sources, chunk parts, output files, and modules while building index maps for fast tree traversal.
The parsing pipeline handles several distinct interface structures representing the core entities of the analyzer binary format.
When an AnalyzeData instance is initialized with an ArrayBuffer, it executes a strict parsing sequence to separate the metadata header from the raw binary block:
new DataView(analyzeArrayBuffer) — Wraps the incoming buffer into a standard DataView interface.analyzeDataView.getUint32(0, false) — Reads the big-endian 32-bit unsigned integer representing the byte length of the leading JSON header string.new Uint8Array(analyzeArrayBuffer, 4, analyzeJsonLength) — Slices out the exact byte range containing the JSON-serialized header payload.TextDecoder('utf-8').decode(...) and JSON.parse(...) — Decodes the UTF-8 byte array into a string and parses it into AnalyzeDataHeader.4 + analyzeJsonLength — Calculates the exact byte offset where the binary section begins.new DataView(analyzeArrayBuffer, analyzeBinaryOffset) — Instantiates the secondary DataView dedicated to reading edge arrays and indices.Note
Edge data references (EdgesDataReference) inside the binary section store compacted adjacency lists prefixed by a u32 count of offset entries, allowing readEdgesDataAtIndex to fetch specific node neighborhoods in $O(1)$ header lookup time without scanning unrequested records.
Source file paths are reconstructed dynamically by walking up the parent chain using getFullSourcePath(index: SourceIndex), which recursively concatenates ancestor paths until reaching a root node where parent_source_index is null.
export function getSpecialModuleType(
analyzeData: AnalyzeData | undefined,
sourceIndex: SourceIndex | null
): SpecialModule | null {
if (!analyzeData || sourceIndex == null) return null
const path = analyzeData.source(sourceIndex)?.path || ''
if (path.endsWith('polyfill-module.js')) {
return SpecialModule.POLYFILL_MODULE
} else if (path.endsWith('polyfill-nomodule.js')) {
return SpecialModule.POLYFILL_NOMODULE
}
return null
}The treemap layout calculation engine transforms raw bundle metrics and hierarchical source trees into structured geometry nodes suitable for rendering. It manages bottom-up metadata precomputation, recursive size accumulation, path folding for single-child directories, descendant counting for collapsed viewports, and proportional subdivision via external layout routines.
When the UI component triggers a layout calculation, execution flows through a precise sequence of transformation steps:
TreemapVisualizer — Initiates the rendering cycle and invokes the top-level layout generator. Sources: apps/bundle-analyzer/components/treemap-visualizer.tsx:8-12computeTreemapLayoutFromAnalyze — Serves as the public entry point that triggers metadata precomputation across the entire source tree before invoking internal generation logic. Sources: apps/bundle-analyzer/lib/treemap-layout.ts:271-290precomputeSourceMetadata — Executes a bottom-up sweep across all sources to aggregate raw byte sizes, compressed sizes, and active filter statuses. Sources: apps/bundle-analyzer/lib/treemap-layout.ts:47-106getOwnSizes — Inspects chunk parts associated with a given source index to compute immediate uncompressed and compressed byte totals. Sources: apps/bundle-analyzer/lib/analyze-data.ts:356-371chunkPart — Retrieves specific binary chunk part structures from the analyzer header to sum file segment sizes. Sources: apps/bundle-analyzer/lib/analyze-data.ts:252-255Sources: apps/bundle-analyzer/lib/analyze-data.ts:252-271, apps/bundle-analyzer/lib/treemap-layout.ts:47-290, apps/bundle-analyzer/components/treemap-visualizer.tsx:8-12
The engine structures output using explicit interfaces defining geometric rectangles, metadata attributes, and node classifications.
Note
Single-child directories whose combined folded path length is 40 characters or fewer are automatically compressed upward by merging their path strings with parent nodes, eliminating redundant nesting layers in the visualizer.
Sources: apps/bundle-analyzer/lib/treemap-layout.ts:47-106, apps/bundle-analyzer/lib/treemap-layout.ts:126-144, apps/bundle-analyzer/lib/treemap-layout.ts:171-201
The TreemapVisualizer component renders hierarchical bundle data onto an HTML5 Canvas, implementing search filtering, LRU-cached text measurement, theme-aware coloring, and spatial mouse interaction. It accepts layout nodes generated by computeTreemapLayoutFromAnalyze and manages interaction states such as hovering, node selection, focusing, and keyboard-driven resets via the Escape key.
Sources: apps/bundle-analyzer/app/page.tsx:96-115, apps/bundle-analyzer/components/treemap-visualizer.tsx:1-34
File coloring is determined by getFileColor(), which inspects module classification flags (js, css, json, asset), environment attributes (server, client, traced), and special module types (SpecialModule). Special polyfill modules receive a dedicated neutral shade, while standard file types map to distinct base colors that are darkened for server environments and selectively lightened for traced modules.
Sources: apps/bundle-analyzer/components/treemap-visualizer.tsx:36-75, apps/bundle-analyzer/lib/types.ts:37-41
Warning
Server-side modules are rendered with colors darkened by 30% using darken(0.3, color). If a server module is also marked as traced, its color is subsequently lightened by 15% using lighten(0.15, color) to restore visual distinguishability.
To maintain high frame rates during rendering, text width calculations utilize an LRU-cached helper (measureTextCached) backed by a Map capped at 30,000 entries. Text strings exceeding available bounding box widths are shortened using truncateTextWithEllipsisIfNeeded(), which performs a binary search across character lengths combined with pre-measured ellipsis dimensions.
User interactions such as clicking or hovering over canvas coordinates are resolved recursively through findNodeAtPosition(). This function tests bounding box containment against LayoutRect coordinates, checking directory title bars before descending into child nodes or returning collapsed directory containers.
Search filtering evaluates active queries against node path hierarchies via searchOriginalTreeForMatch() and nodeOrDescendantsMatchSearch(). When matching against collapsed directories, the traversal checks the original underlying tree structure to ensure descendant matches correctly illuminate parent containers.
Tip
Pressing the Escape key while focus is outside text input elements immediately resets both the selected source index and the focused source index back to the analysis root node.
The module graph and dependency resolution system evaluates module indices, trace dependencies, and route entry points via Breadth-First Search traversal. It resolves active entries by scanning module identifiers against known Next.js template paths and client entry points.
Sources: apps/bundle-analyzer/lib/module-graph.ts:12-66, apps/bundle-analyzer/lib/module-graph.ts:69-154
Active entry points are discovered via computeActiveEntries(), which inspects module identifiers against a predefined set of internal Next.js build templates and turbopack client entry points.
export function computeActiveEntries(
modulesData: ModulesData,
analyzeData: AnalyzeData
): ModuleIndex[] {
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',
]
const potentialEntries = [
'next/dist/client/app-next-turbopack.js',
'next/dist/client/next-turbopack.js',
]
// ...
}The function computeModuleDepthMap() coordinates traversal from active entries to assign relative distance weights across the graph. Regular and traced dependencies receive an increment of depth + 1, whereas asynchronous dependencies receive a depth penalty increment of depth + 1000 and are deferred into descending-sorted queues.
Caution
Async module dependencies are deferred using a priority queue sorted by depth descending (b.depth - a.depth). Direct insertion into the main depth map risks processing async nodes before their parent modules, leading to corrupted depth metrics.
The import chain and selection details subsystem powers the contextual sidebar inspector and interactive import trace tree. It parses breadcrumb paths using segment matchers, resolves module indices from source paths, and displays granular resource metrics alongside output chunk mappings.
Sources: apps/bundle-analyzer/components/import-chain.tsx:128-160, apps/bundle-analyzer/components/sidebar.tsx:38-133
The traversal from an import component down to the underlying source identifier follows a structured execution sequence:
ImportChain initiates rendering and path resolution.
Sources: apps/bundle-analyzer/components/import-chain.tsx:141-145
getModuleIndicesFromSourceIndex accepts a sourceIndex and queries the analyzer data structure.
Sources: apps/bundle-analyzer/components/import-chain.tsx:141-145
getFullSourcePath walks up the parent source index chain recursively to reconstruct the complete path string.
Sources: apps/bundle-analyzer/lib/analyze-data.ts:344-354
source retrieves the raw AnalyzeSource entry from the internal headers array using the final index.
Sources: apps/bundle-analyzer/lib/analyze-data.ts:240-242
Sources: apps/bundle-analyzer/components/import-chain.tsx:141-145, apps/bundle-analyzer/lib/analyze-data.ts:240-242, apps/bundle-analyzer/lib/analyze-data.ts:344-354
Path segments are parsed using regular expressions to categorize components into common paths, package names, and infrastructure directories. The getPathParts function evaluates current segments against previous paths to highlight shared hierarchies.
Note
When node_modules/ is detected within path segments, package name identification automatically accounts for scoped packages by claiming two segments when a segment starts with @.
The sidebar component (Sidebar) renders conditional inspection details based on the currently selected source index. If a source represents a built-in polyfill, it displays specific badge attributes; otherwise, it embeds the ImportChain component and lists associated output chunks.
function SelectionDetails({
analyzeData,
modulesData,
selectedSourceIndex,
filterSource,
moduleDepthMap,
environmentFilter,
}: {
analyzeData: AnalyzeData
modulesData: ModulesData | null
selectedSourceIndex: number
moduleDepthMap: Map<number, number>
environmentFilter: 'client' | 'server'
filterSource: (sourceIndex: number) => boolean
}) {
const specialModuleType = getSpecialModuleType(
analyzeData,
selectedSourceIndex
)
const selectedSource =
selectedSourceIndex != null
? analyzeData.source(selectedSourceIndex)
: undefined
const hasChildModules =
selectedSourceIndex != null &&
analyzeData.sourceChildren(selectedSourceIndex).length > 0
const { size, compressedSize } = analyzeData.getRecursiveSizes(
selectedSourceIndex,
filterSource
)
// ...
}