Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/calm-assets-match.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"webpack-bundle-analyzer": patch
---

Improve report generation performance for compilations with many assets.
82 changes: 68 additions & 14 deletions src/analyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,54 @@ function getBundleModules(bundleStats) {
});
}

/**
* @typedef {{ module: StatsModule, index: number }} IndexedModule
*/

/**
* @param {StatsModule[]} modules modules
* @returns {Map<string | number, IndexedModule[]>} modules indexed by chunk ID
*/
function getModulesByChunk(modules) {
/** @type {Map<string | number, IndexedModule[]>} */
const modulesByChunk = new Map();

for (const [index, statsModule] of modules.entries()) {
for (const chunk of statsModule.chunks || []) {
let chunkModules = modulesByChunk.get(chunk);

if (!chunkModules) {
chunkModules = [];
modulesByChunk.set(chunk, chunkModules);
}

chunkModules.push({ module: statsModule, index });
}
}

return modulesByChunk;
}

/**
* @param {StatsAsset} statsAsset stats asset
* @param {Map<string | number, IndexedModule[]>} modulesByChunk modules indexed by chunk ID
* @returns {StatsModule[]} modules belonging to the asset in their original order
*/
function getAssetModulesByChunk(statsAsset, modulesByChunk) {
/** @type {Map<StatsModule, IndexedModule>} */
const indexedModules = new Map();

for (const chunk of statsAsset.chunks || []) {
for (const indexedModule of modulesByChunk.get(chunk) || []) {
indexedModules.set(indexedModule.module, indexedModule);
}
}

return [...indexedModules.values()]
.toSorted((a, b) => a.index - b.index)
.map(({ module }) => module);
}

/** @typedef {Record<string, Record<string, boolean>>} ChunkToInitialByEntrypoint */

/**
Expand Down Expand Up @@ -316,16 +364,28 @@ function getViewerData(bundleStats, bundleDir, opts) {

/** @typedef {{ size: number, parsedSize?: number, gzipSize?: number, brotliSize?: number, zstdSize?: number, modules: StatsModule[], tree: Folder }} Asset */

const rootModules = getBundleModules(bundleStats);
const rootModulesByChunk = getModulesByChunk(rootModules);

const assets = bundleStats.assets.reduce((result, statAsset) => {
// If asset is a childAsset, then calculate appropriate bundle modules by looking through stats.children
const assetBundles = statAsset.isChild
? getChildAssetBundles(bundleStats, statAsset.name)
: bundleStats;
/** @type {StatsModule[]} */
const modules = assetBundles
? // @ts-expect-error TODO looks like we have a bug with child compilation parsing, need to add test cases
getBundleModules(assetBundles)
: [];
let assetModules;

if (statAsset.isChild) {
// Preserve child-compilation matching because child assets use a different module list.
const assetBundles = getChildAssetBundles(bundleStats, statAsset.name);
/** @type {StatsModule[]} */
const modules = assetBundles
? // @ts-expect-error TODO looks like we have a bug with child compilation parsing, need to add test cases
getBundleModules(assetBundles)
: [];
Comment thread
christiango marked this conversation as resolved.
assetModules = modules.filter((statModule) =>
assetHasModule(statAsset, statModule),
);
} else {
assetModules = getAssetModulesByChunk(statAsset, rootModulesByChunk);
}

const asset = (result[statAsset.name] = /** @type {Asset} */ ({
size: statAsset.size,
}));
Expand All @@ -350,12 +410,6 @@ function getViewerData(bundleStats, bundleDir, opts) {
}
}

// Picking modules from current bundle script
/** @type {StatsModule[]} */
let assetModules = (modules || []).filter((statModule) =>
assetHasModule(statAsset, statModule),
);

// Adding parsed sources
if (parsedModules) {
/** @type {StatsModule[]} */
Expand Down
69 changes: 69 additions & 0 deletions test/analyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const fs = require("node:fs");
const path = require("node:path");
const url = require("node:url");
const puppeteer = require("puppeteer");
const { getViewerData } = require("../src/analyzer");
const { isZstdSupported } = require("../src/sizeUtils");

let browser;
Expand Down Expand Up @@ -133,6 +134,74 @@ describe("Analyzer", () => {
);
});

it("should preserve module order and deduplicate modules for multi-chunk assets", () => {
const chartData = getViewerData(
{
assets: [
{
name: "multi-chunk.js",
size: 5,
chunks: ["string-chunk", 1],
},
],
chunks: [],
entrypoints: {},
modules: [
{
id: 1,
identifier: "./numeric.js",
name: "./numeric.js",
size: 1,
chunks: [1],
},
{
id: 2,
identifier: "./shared.js",
name: "./shared.js",
size: 1,
chunks: [1, "string-chunk"],
},
{
id: 3,
identifier: "./string.js",
name: "./string.js",
size: 1,
chunks: ["string-chunk"],
},
{
id: 4,
identifier: "./unchunked.js",
name: "./unchunked.js",
size: 1,
chunks: [],
},
{
id: 1,
identifier: "./duplicate-id.js",
name: "./duplicate-id.js",
size: 1,
chunks: ["string-chunk"],
},
{
id: 5,
identifier: "webpack/runtime/test",
name: "webpack/runtime/test",
moduleType: "runtime",
size: 1,
chunks: [1],
},
],
},
null,
);

expect(chartData[0].groups.map((group) => group.label)).toEqual([
"numeric.js",
"shared.js",
"string.js",
]);
});

it("should record accurate byte lengths for sources with special chars", async () => {
generateReportFrom("with-special-chars/stats.json");
const chartData = await getChartData();
Expand Down