writing

webpack-stats.json explained — assets, chunks, and modules

What webpack actually puts in webpack-stats.json: assets vs chunks vs modules, how the reasons array traces why a module ended up in your bundle, and how to generate and read it.

webpack stats JSON is one of those files almost every front-end developer has seen referenced in a tool's README and never actually opened. The raw output is routinely 10–20 MB, full of compiler internals, and not designed to be read by hand. But understanding its structure is worth the effort, because it is the most information-dense record webpack produces of what actually shipped and why.

How to generate it

The simplest path is the CLI flag:

webpack --profile --json > dist/webpack-stats.json

--profile adds per-module timing data on top of the size and dependency graph. If you are using webpack-bundle-analyzer, you can have it generate the file as a side effect without opening a browser:

// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'disabled',
      generateStatsFile: true,
      statsFilename: 'dist/webpack-stats.json',
    }),
  ],
};

Both Next.js and Angular emit webpack-compatible stats. For Angular, pass --stats-json to the build:

ng build --stats-json

The output lands alongside the compiled assets. The format is the same across all three — which is why tooling can parse any of them without caring how the project is structured.

The anatomy of the file

webpack-stats.json is an object. Four top-level keys carry almost all the useful information.

assets is the list of files webpack emitted to disk. Each entry has a name, a size in bytes (uncompressed), and a chunkNames array that names the chunk the asset belongs to. This is your ground truth for what actually shipped.

chunks describes the units of code-splitting. A chunk has an id, a names array (for named chunks like "vendor" or "main"), a files array (which assets on disk it produced), a size, and an initial boolean that distinguishes eagerly loaded chunks from lazy ones. Understanding chunks is understanding how your code-splitting decisions translated into actual output files.

modules is where most of the investigative value lives. Each module represents a source file or dependency that was included in the bundle. The fields that matter most are name (the resolved file path), size (its contribution after tree-shaking), chunks (which chunk IDs include it), and reasons — a list of other modules that caused this one to be pulled in.

entrypoints maps entry point names to the chunk IDs they depend on. It is the starting point for tracing which chunks a given page or route loads.

The difference between an asset, a chunk, and a module

The three concepts sit at different levels of abstraction, and conflating them is the most common source of confusion when reading the file.

A module is a source input: a TypeScript file, a CSS file, or a file inside node_modules. It is what your code imports.

A chunk is a grouping of modules that webpack decided should travel together. Code-splitting produces multiple chunks; dynamic import() calls create new ones. One chunk can produce multiple assets (e.g. a JS file and its source map).

An asset is an actual output file on disk — the thing the browser downloads.

When you ask "what is in my vendor bundle?", you are looking at an asset. When you ask "why is this dependency here?", you are looking at module reasons.

Reading it to answer real questions

The questions worth answering are almost always one of three kinds.

What is in a particular chunk? Filter modules by the chunk ID that corresponds to your chunk. For a vendor chunk, you will see every node_modules path that ended up there, along with each module's size. Sort by size descending and the heaviest dependencies surface immediately.

Why is this dependency included? Find the module by its path in modules, then read its reasons array. Each reason entry has a moduleName field — the file that imported it — and a type field describing the relationship. Following the chain of reasons will always lead you back to your own application code, which is where the fix lives.

Which entry point loads which chunks? Start at entrypoints, pull the chunk IDs for the entry you care about, then trace those IDs back through chunks to their files.

Why you feed it to a tool

The raw file is not built to be read directly. A medium-sized app produces a modules array with thousands of entries and reasons graphs that can loop several levels deep. The actual structure is also not stable across webpack versions — minor releases occasionally rename fields or restructure nested objects.

The sensible workflow is to let a parser handle the mechanics and answer the structural questions through a visual interface. The free analyzer at /analyze reads a webpack stats file with no account needed and renders the treemap immediately. dendrobundle does the same on ingest and layers on a per-commit history so you can see when something changed, not just how big it is today. The supported formats reference covers the exact fields dendrobundle reads from the stats file, and the webpack guide walks through wiring it into a build.

The file is not mysterious once you know what it contains. It is verbose because it is complete — every decision the compiler made is recorded somewhere in it.