Skip to content
This repository was archived by the owner on Sep 7, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,49 @@ them off the graph is never built at all, so the rest of the app stays quick.
3. Open the analyse app and load that file.
4. Inspect modules, chunks, and assets to find large bundles or suspicious dependency patterns.

## Troubleshooting

### `Cannot read property 'parents' of undefined` when opening the modules tab

The stats file has modules but no chunks. Every module still names the chunk
ids it belongs to, and the app follows those ids into a chunk list that is not
there ([#34](https://github.com/webpack/analyse/issues/34)). The chunk views
are empty for the same reason.

webpack leaves them out when it is configured with `stats: { chunks: false }`,
and a tool that writes the stats for you can pass a filtered subset of its own.
The simplest fix is to let webpack write everything:

```bash
npx webpack --profile --json > stats.json
```

From the config, keep at least the parts this app reads:

```js
// webpack.config.js
module.exports = {
stats: {
modules: true,
reasons: true, // what pulls each module in
chunks: true,
chunkOrigins: true, // what asked for each chunk
assets: true
}
};
```

`stats: { all: true }` (webpack 5) or the `verbose` preset turns on everything,
which is more than the app needs but never less. With
[webpack-stats-plugin](https://github.com/FormidableLabs/webpack-stats-plugin),
pass the same through its `stats` option, since its default is a small subset.

### The graph never appears, or the tab hangs on a large build

The graphs are off above 5000 modules or chunks, because laying one out that
big can take the tab down with it. The control under the graph turns them on
and off, and remembers the choice. See [Reading the graphs](#reading-the-graphs).

## Notes

- The app expects the webpack stats JSON, not a raw asset bundle.
Expand Down
8 changes: 8 additions & 0 deletions app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ function load(stats) {
});
stats.modules.forEach(function(module) {
module.reasons = module.reasons || [];
module.chunks = module.chunks || [];
module.reasons.forEach(function(reason) {
var m = mapModulesIdent["$" + reason.moduleIdentifier];
if (!m) return;
Expand Down Expand Up @@ -59,8 +60,15 @@ function load(stats) {
})(module);
});
stats.chunks.forEach(function(chunk) {
// Which of these a stats file carries depends on the options it was
// written with, and a parent can name a chunk that was left out of it.
chunk.parents = chunk.parents || [];
chunk.origins = chunk.origins || [];
chunk.names = chunk.names || [];
chunk.files = chunk.files || [];
chunk.parents.forEach(function(parent) {
var c = mapChunks[parent];
if (!c) return;
c.children.push(chunk.id);
});
chunk.origins.forEach(function(origin) {
Expand Down
5 changes: 4 additions & 1 deletion app/graphs/chunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ app.stats.chunks.forEach(function (chunk, idx) {
"[" +
chunk.id +
"] " +
chunk.origins
(chunk.origins || [])
.map(function (o) {
return (o.reasons || [])
.concat(o.name)
Expand All @@ -48,7 +48,10 @@ app.stats.chunks.forEach(function (chunk, idx) {
});
});
app.stats.chunks.forEach(function (chunk) {
// app.load fills these in, but the graph is built from whatever the stats
// hold: a parent can name a chunk that is not in the file at all.
chunk.parents.forEach(function (parent) {
if (!app.mapChunks[parent]) return;
edges.push({
id: "edge" + chunk.id + "-" + parent,
source: "chunk" + parent,
Expand Down
8 changes: 7 additions & 1 deletion app/graphs/modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,13 @@ app.stats.modules.forEach(function(module, idx) {
});
if (chunks.length === 0) return false;
return chunks.some(function(c) {
return isInChunks(app.mapChunks[c].parents, checked.concat(c));
// A stats file written with `chunks: false` still names chunk
// ids on the modules while leaving the chunks themselves out,
// so this lookup can come back empty (webpack/analyse#34).
var parent = app.mapChunks[c];
return parent
? isInChunks(parent.parents || [], checked.concat(c))
: false;
});
})(parentModule.chunks, []);
});
Expand Down
3 changes: 3 additions & 0 deletions app/pages/chunk/missing.pug
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.container-fluid: .row: .col-md-12: .well
h4 chunk #{id}
p This chunk is not in the stats file. Chunks are left out when webpack is run with <code>stats.chunks = false</code>, while the modules still name the chunks they belong to. Generate the stats again with chunks included to see this one.
14 changes: 8 additions & 6 deletions app/pages/chunk/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ module.exports = function (id) {
id = isNaN(parseInt(id, 10)) ? decodeURIComponent(id) : parseInt(id, 10);
document.title = "chunk " + id;
sortableTable.enable();
// The module table links to every chunk a module names, and those ids can
// point at chunks a stats file leaves out (webpack/analyse#34).
var chunk = app.mapChunks[id];
$(".page").html(
require("./chunk.pug")({
stats: app.stats,
id: id,
chunk: app.mapChunks[id],
})
chunk
? require("./chunk.pug")({ stats: app.stats, id: id, chunk: chunk })
: require("./missing.pug")({ id: id })
);
modulesGraph.show();
modulesGraph.setActiveChunk(id);
if (chunk) modulesGraph.setActiveChunk(id);
else modulesGraph.setNormal();
return function () {
modulesGraph.hide();
};
Expand Down
6 changes: 6 additions & 0 deletions app/pages/chunks/chunks.pug
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
if stats.chunks.length === 0
p.text-muted.
No chunks in this stats file. webpack leaves them out when it is run
with #[code stats.chunks = false], while the modules still name the
chunks they belong to; generate the stats again with chunks included
to see them here.
table.table.table-condensed
thead
tr
Expand Down
77 changes: 77 additions & 0 deletions test/appLoad.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Checks that reading a stats file survives the parts a stats file is allowed
// to leave out (webpack/analyse#34). Run with `npm test`.
var test = require("node:test");
var assert = require("node:assert");

var app = require("../app/app");

function load(stats) {
// Loading reports to google analytics, which falls back to console.log
// outside the browser.
var log = console.log;
console.log = function() {};
try {
app.load(stats);
} finally {
console.log = log;
}
return app.stats;
}

function module_(id, chunks) {
return {
id: id,
identifier: "/m" + id + ".js",
name: "./m" + id + ".js",
size: 100,
chunks: chunks
};
}

test("reads a stats file written with chunks: false", function() {
// webpack leaves the chunks out but still names them on every module,
// which is what used to throw on "parents of undefined".
var stats = load({
modules: [module_(1, [0]), module_(2, [1])]
});
assert.deepStrictEqual(stats.chunks, []);
assert.deepStrictEqual(stats.modules[0].chunks, [0], "the ids are kept");
assert.strictEqual(app.mapChunks[0], undefined, "and lead nowhere");
});

test("survives a chunk whose parent is not in the file", function() {
var stats = load({
modules: [module_(1, [1])],
chunks: [
{ id: 1, size: 100, parents: [7], names: ["main"], files: ["main.js"] }
]
});
assert.deepStrictEqual(stats.chunks[0].children, [], "nothing to link to");
assert.deepStrictEqual(stats.chunks[0].parents, [7], "the id is kept");
});

test("fills in the chunk fields a stats file can omit", function() {
var stats = load({ modules: [], chunks: [{ id: 0, size: 10 }] });
var chunk = stats.chunks[0];
assert.deepStrictEqual(chunk.parents, []);
assert.deepStrictEqual(chunk.origins, []);
assert.deepStrictEqual(chunk.names, []);
assert.deepStrictEqual(chunk.files, []);
assert.deepStrictEqual(chunk.children, []);
});

test("fills in the module fields a stats file can omit", function() {
var stats = load({
modules: [{ id: 1, identifier: "/m1.js", name: "./m1.js", size: 100 }]
});
assert.deepStrictEqual(stats.modules[0].chunks, []);
assert.deepStrictEqual(stats.modules[0].reasons, []);
assert.deepStrictEqual(stats.modules[0].dependencies, []);
});

test("reads a stats file with nothing in it at all", function() {
var stats = load({});
assert.deepStrictEqual(stats.modules, []);
assert.deepStrictEqual(stats.chunks, []);
assert.deepStrictEqual(stats.assets, []);
});