Webpack's code splitting mechanism distributes application code across multiple bundles to eliminate redundancy and improve cache hit rates. When multiple chunks import identical dependencies, those modules get duplicated in the output. Extracting shared modules into isolated files reduces payload sizes and allows browsers to cache infrequently changing vendor code separately from application logic.
When to Split Bundles
Splitting becomes essential when numerous entry points or asynchronous boundaries depend on sizable shared libraries. Typical situations include:
- Large single-page applications: Frameworks and utility libraries bloat entry points. Isolating them prevents redundant downloads across routes.
- Multi-page builds: Distinct HTML entries often share heavy dependencies. Separate vendor bundles remain cached when users navigate between pages.
- On-demand features: Code gated by user roles or runtime flags can be deferred until requested, shortening initial parse time.
- Faster rebuilds: Externalizing stable third-party code from frequently changing business logic shrinks incremental compilation and deployment sizes.
Manual Splitting with DLLs
Manual splitting requires pre-bundling shared modules into Dynamic Link Libraries (DLLs). The main build then references these compiled assets instead of re-bundling them.
Pre-compile common dependencies:
const path = require('path');
const webpack = require('webpack');
module.exports = {
mode: 'production',
entry: {
reactStack: ['react', 'react-dom'],
toolset: ['axios', 'dayjs']
},
output: {
filename: 'libs/[name].[fullhash:6].js',
path: path.resolve(__dirname, 'dist'),
library: { name: '__lib_[name]', type: 'var' }
},
plugins: [
new webpack.DllPlugin({
name: '__lib_[name]',
path: path.resolve(__dirname, 'manifests', '[name].json')
})
]
};
Add the DLL build script:
{
"scripts": {
"build:dll": "webpack --config webpack.libs.config.js"
}
}
This generates manifests inside a manifests/ directory and compiled assets under dist/libs/.
Link DLLs in the HTML template so globals are available at runtime:
<script src="./libs/reactStack.js"></script>
<script src="./libs/toolset.js"></script>
Prevent clean-up plugins from removing the pre-generated libraries:
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ['**/*', '!libs', '!libs/**']
})
]
};
Reference the manifests during the primary build so Webpack omits those modules:
const webpack = require('webpack');
const reactManifest = require('./manifests/reactStack.json');
const toolsManifest = require('./manifests/toolset.json');
module.exports = {
plugins: [
new webpack.DllReferencePlugin({ manifest: reactManifest }),
new webpack.DllReferencePlugin({ manifest: toolsManifest })
]
};
Manual DLLs suit stable, heavyweight dependencies. Small libraries add more HTTP overhead than they save and should remain in the main bundle.
Automatic Splitting with SplitChunks
Automatic splitting delegates decisions to Webpack's SplitChunksPlugin. Instead of manually declaring which modules to extract, you define heuristics that the compiler applies across the dependency graph.
Enable the behavior through the optimization key:
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000,
minChunks: 1,
cacheGroups: {
vendorGroup: {
test: /[\\/]node_modules[\\/]/,
priority: 10,
name: 'vendors'
},
sharedModules: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
};
Key Configuration Options
-
chunks: Determines which chunk types participate.all— inspects both synchronous and asynchronous chunks.async— default; only splits dynamically imported modules.initial— only entry chunks.
-
minChunks: Minimum number of chunks that must share a module before it is extracted. Raising this to2or higher ensures only truly common code is lifted out. -
minSize: Smallest byte size a candidate chunk must reach before being emitted. The default is roughly 20 kB. -
maxSize: Attempts to further subdivide chunks that exceed the threshold. Because modules are atomic, a single module larger than the limit cannot be broken apart. This option does not reduce total transfer size; it merely creates more parallel requests.
Cache Groups
Cache groups act as isolated rule sets evaluated in priority order. Once a module is claimed by a group, subsequent groups skip it.
The previous example defined two groups. The first captures everything under node_modules into a vendors bundle. The second captures any module imported at least twice that was not already handled.
You can also extract styles via cache groups:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
appStyles: {
type: 'css/mini-extract',
enforce: true,
name: 'styles'
}
}
}
},
module: {
rules: [
{
test: /\.s?css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css',
chunkFilename: '[name].[contenthash:8].css'
})
]
};
How Automatic Splitting Works
During compilation, Webpack performs the following:
- Evaluates every chunk's module list against the active strategy.
- Identifies modules that satisfy the configured thresholds.
- Generates new chunks containing those modules and updates the runtime bootstrap code.
- Removes the extracted code from original chunks and replaces it with lightweight module references.
The resulting architecture lets the browser download shared code once and execute it across multiple entry points without re-parsing the same source.