How to Speed Up Webpack Builds: A Deep Dive into SplitChunksPlugin Optimization
This article explains why a large project’s Webpack bundle became painfully slow, walks through detailed bundle analysis, shows before‑and‑after configuration changes—including setting maxAsyncRequests—and explores the inner workings of SplitChunksPlugin, its default settings, attributes, execution flow, and chunk‑splitting strategy, providing code examples and diagrams for better understanding.
Research Background
Last month the team reported that a project’s packaging speed was getting slower, taking at least half an hour per build, which was unacceptable for both developers and testers. The project was therefore optimized.
Bundle Analysis
Before Optimization
The current splitChunks.cacheGroups configuration is as follows:
{
styles: {
name: 'style',
test: m => m.constructor.name === 'CssModule',
chunks: 'all',
enforce: true,
priority: 40,
},
emcommon: {
name: 'emcommon',
test: module => {
const regs = [/@ant-design/, /@em/, /@bytedesign/];
return regs.some(reg => reg.test(module.context));
},
chunks: 'all',
enforce: true,
priority: 30,
},
byteedu: {
name: 'byteedu',
test: module => {
const regs = [/@ax/, /@bridge/, /axios/, /lodash/, /@byted-edu/, /codemirror/, /@syl-editor/, /prosemirror/];
return regs.some(reg => reg.test(module.context));
},
chunks: 'all',
enforce: true,
priority: 20,
},
default: {
minChunks: 2,
priority: 1,
chunks: 'all',
reuseExistingChunk: true,
},
};Before optimization the production build took 14 minutes. Bundle analysis showed many pages repeatedly bundled libraries such as arco-design , and several chunks that should have been extracted were not, causing a huge output size. The root cause was that the default configuration did not take effect; modules shared by two or more chunks were expected to be extracted, but they were not.
After Optimization
After discovering that the default configuration was ineffective, we modified it and found that setting maxAsyncRequests to 30 made the default configuration work. The shared chunk size dropped to about 30 MB, a 90 % reduction, and the online build time shortened to roughly 2.5 minutes. This raises questions: why does maxAsyncRequests enable the expected splitting, what does it mean, and how does Webpack’s splitting logic actually work? The answers require digging into Webpack’s source code.
default: {
maxAsyncRequests: 30,
minChunks: 2,
priority: 1,
chunks: 'all',
reuseExistingChunk: true,
}Module and Chunk Relationship
Because the terms “module” and “chunk” appear frequently, we first clarify their relationship:
When we write files—whether ESM, CommonJS, or AMD—they are modules .
When these source modules are processed by Webpack, it generates chunks based on the import graph.
SplitChunksPlugin
Our project uses Webpack 4.44.2, so we examine the source of this version to understand how SplitChunksPlugin performs bundling.
The plugin introduces cache groups to group modules into chunks. The default optimization of Webpack is implemented via SplitChunksPlugin configuration (see the official documentation).
Default Configuration
Even without any custom configuration, Webpack applies default rules in production mode. The relevant source in WebpackOptionsDefaulter.js (lines 226‑256) defines these defaults:
this.set("optimization.splitChunks", {});
this.set("optimization.splitChunks.hidePathInfo", "make", options => {
return isProductionLikeMode(options);
});
this.set("optimization.splitChunks.chunks", "async");
this.set("optimization.splitChunks.minSize", "make", options => {
return isProductionLikeMode(options) ? 30000 : 10000;
});
this.set("optimization.splitChunks.minChunks", 1);
this.set("optimization.splitChunks.maxAsyncRequests", "make", options => {
return isProductionLikeMode(options) ? 5 : Infinity;
});
this.set("optimization.splitChunks.maxInitialRequests", "make", options => {
return isProductionLikeMode(options) ? 3 : Infinity;
});
this.set("optimization.splitChunks.automaticNameDelimiter", "~");
this.set("optimization.splitChunks.name", true);
this.set("optimization.splitChunks.cacheGroups", {});
this.set("optimization.splitChunks.cacheGroups.default", {
automaticNamePrefix: "",
reuseExistingChunk: true,
minChunks: 2,
priority: -20
});
this.set("optimization.splitChunks.cacheGroups.vendors", {
automaticNamePrefix: "vendors",
test: /[\\/]node_modules[\\/]/,
priority: -10
});Key points of the default configuration:
chunks : Determines which chunks are optimized (values: all, async, initial).
minSize : New chunks must be larger than 30 KB in production.
minChunks : Minimum number of chunks that share a module.
maxAsyncRequests : Maximum number of parallel async requests.
maxInitialRequests : Maximum number of parallel initial requests.
automaticNameDelimiter : Delimiter used when generating chunk names.
name : Whether to generate a name automatically.
cacheGroups : Defines groups of modules to be split into separate chunks, each with its own rules (e.g., test, priority, reuseExistingChunk).
Execution Flow
The Webpack compilation process follows these steps:
Parse configuration and command‑line options with yargs.
Initialize the compiler and built‑in plugins.
Start compilation, creating a Compilation object.
Build modules starting from entry files.
Seal phase: generate chunks, apply optimizations, and emit assets.
Plugins hook into the compilation via an apply method. SplitChunksPlugin listens to the optimizeChunksAdvanced hook to perform its splitting logic.
apply(compiler) {
compiler.hooks.thisCompilation.tap("SplitChunksPlugin", compilation => {
let alreadyOptimized = false;
compilation.hooks.unseal.tap("SplitChunksPlugin", () => {
alreadyOptimized = false;
});
compilation.hooks.optimizeChunksAdvanced.tap(
"SplitChunksPlugin",
chunks => {
// core splitting logic
}
);
});
}Chunk Splitting Strategy Steps
The strategy is divided into three phases:
Preparation Phase
Before optimization, necessary data structures and helper methods are created, such as indexMap, chunksInfoMap, and functions to generate unique keys for chunk groups.
const indexMap = new Map();
let index = 1;
for (const chunk of chunks) {
indexMap.set(chunk, index++);
}
const getKey = chunks => {
return Array.from(chunks, c => indexMap.get(c))
.sort((a, b) => a - b)
.join();
};
const chunkSetsInGraph = new Map();
for (const module of compilation.modules) {
const chunksKey = getKey(module.chunksIterable);
if (!chunkSetsInGraph.has(chunksKey)) {
chunkSetsInGraph.set(chunksKey, new Set(module.chunksIterable));
}
}Module Grouping Phase
All modules are iterated, cache groups are resolved, and for each combination of chunks that satisfies the cache‑group rules, addModuleToChunksInfoMap stores the grouping information in chunksInfoMap.
for (const module of compilation.modules) {
let cacheGroups = this.options.getCacheGroups(module);
if (!Array.isArray(cacheGroups) || cacheGroups.length === 0) continue;
const chunksKey = getKey(module.chunksIterable);
let combs = combinationsCache.get(chunksKey);
if (combs === undefined) {
combs = getCombinations(chunksKey);
combinationsCache.set(chunksKey, combs);
}
let cacheGroupIndex = 0;
for (const cacheGroupSource of cacheGroups) {
// build cacheGroup object with merged defaults
const cacheGroup = { /* ... */ };
for (const chunkCombination of combs) {
if (chunkCombination.size < cacheGroup.minChunks) continue;
const { chunks: selectedChunks, key: selectedChunksKey } = getSelectedChunks(
chunkCombination,
cacheGroup.chunksFilter
);
addModuleToChunksInfoMap(
cacheGroup,
cacheGroupIndex,
selectedChunks,
selectedChunksKey,
module
);
}
cacheGroupIndex++;
}
}Check Phase
Each entry in chunksInfoMap is examined against the cache‑group constraints (size, request limits, etc.). Valid groups are turned into new chunks, modules are moved, and the graph is updated.
for (const [key, info] of chunksInfoMap) {
if (info.cacheGroup._validateSize && info.size < info.cacheGroup.minSize) {
chunksInfoMap.delete(key);
}
}
while (chunksInfoMap.size > 0) {
// find best entry
let bestEntryKey, bestEntry;
for (const [key, info] of chunksInfoMap) {
if (!bestEntry) { bestEntry = info; bestEntryKey = key; }
else if (compareEntries(bestEntry, info) < 0) { bestEntry = info; bestEntryKey = key; }
}
const item = bestEntry;
chunksInfoMap.delete(bestEntryKey);
// create or reuse chunk, move modules, update sizes, etc.
// (full logic omitted for brevity)
}After this phase, all cache groups that meet the configuration are emitted as separate chunks, completing the code‑splitting process.
Conclusion
The article walked through the entire workflow of SplitChunksPlugin —from preparation, module grouping, to final checks—showing how each step contributes to effective bundle splitting. While the plugin’s source cannot be modified, configuring appropriate cacheGroups allows developers to control the resulting chunks and achieve significant build‑time and size improvements.
References
Official documentation: https://v4.webpack.js.org/plugins/split-chunks-plugin/#root
Official API: https://v4.webpack.js.org/api/compilation-hooks/#unseal
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
