This guide extends the example provided inGetting Started. Please make sure you are at least familiar with the example provided there and theOutput Management chapter.
Code splitting is one of the most compelling features of webpack. This feature allows you to split your code into various bundles which can then be loaded on demand or in parallel. It can be used to achieve smaller bundles and control resource load prioritization which, if used correctly, can have a major impact on load time.
There are three general approaches to code splitting available:
entry configuration.SplitChunksPlugin to dedupe and split chunks.This is by far the easiest and most intuitive way to split code. However, it is more manual and has some pitfalls we will go over. Let's take a look at how we might split another module from the main bundle:
project
webpack-demo|- package.json|- package-lock.json|- webpack.config.js|- /dist|- /src |- index.js+ |- another-module.js|- /node_modulesanother-module.js
import _from'lodash';console.log(_.join(['Another','module','loaded!'],' '));webpack.config.js
const path = require('path');module.exports = {- entry: './src/index.js',+ mode: 'development',+ entry: {+ index: './src/index.js',+ another: './src/another-module.js',+ }, output: {- filename: 'main.js',+ filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), },};This will yield the following build result:
...[webpack-cli] Compilation finishedasset index.bundle.js553 KiB[emitted](name: index)asset another.bundle.js553 KiB[emitted](name: another)runtime modules2.49 KiB12 modulescacheable modules530 KiB ./src/index.js257 bytes[built][code generated] ./src/another-module.js84 bytes[built][code generated] ./node_modules/lodash/lodash.js530 KiB[built][code generated]webpack5.4.0 compiled successfullyin245 msAs mentioned there are some pitfalls to this approach:
The first of these two points is definitely an issue for our example, aslodash is also imported within./src/index.js and will thus be duplicated in both bundles. Let's remove this duplication in next section.
ThedependOn option allows to share the modules between the chunks:
webpack.config.js
const path = require('path');module.exports = { mode: 'development', entry: {- index: './src/index.js',- another: './src/another-module.js',+ index: {+ import: './src/index.js',+ dependOn: 'shared',+ },+ another: {+ import: './src/another-module.js',+ dependOn: 'shared',+ },+ shared: 'lodash', }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), },};If we're going to use multiple entry points on a single HTML page,optimization.runtimeChunk: 'single' is needed too, otherwise we could get into trouble describedhere.
webpack.config.js
const path = require('path');module.exports = { mode: 'development', entry: { index: { import: './src/index.js', dependOn: 'shared', }, another: { import: './src/another-module.js', dependOn: 'shared', }, shared: 'lodash', }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), },+ optimization: {+ runtimeChunk: 'single',+ },};And here's the result of build:
...[webpack-cli] Compilation finishedasset shared.bundle.js549 KiB[comparedfor emit](name: shared)asset runtime.bundle.js7.79 KiB[comparedfor emit](name: runtime)asset index.bundle.js1.77 KiB[comparedfor emit](name: index)asset another.bundle.js1.65 KiB[comparedfor emit](name: another)Entrypoint index1.77 KiB= index.bundle.jsEntrypoint another1.65 KiB= another.bundle.jsEntrypoint shared557 KiB= runtime.bundle.js7.79 KiB shared.bundle.js549 KiBruntime modules3.76 KiB7 modulescacheable modules530 KiB ./node_modules/lodash/lodash.js530 KiB[built][code generated] ./src/another-module.js84 bytes[built][code generated] ./src/index.js257 bytes[built][code generated]webpack5.4.0 compiled successfullyin249 msAs you can see there's anotherruntime.bundle.js file generated besidesshared.bundle.js,index.bundle.js andanother.bundle.js.
Although using multiple entry points per page is allowed in webpack, it should be avoided when possible in favor of an entry point with multiple imports:entry: { page: ['./analytics', './app'] }. This results in a better optimization and consistent execution order when usingasync script tags.
TheSplitChunksPlugin allows us to extract common dependencies into an existing entry chunk or an entirely new chunk. Let's use this to de-duplicate thelodash dependency from the previous example:
webpack.config.js
const path = require('path'); module.exports = { mode: 'development', entry: { index: './src/index.js', another: './src/another-module.js', }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), },+ optimization: {+ splitChunks: {+ chunks: 'all',+ },+ }, };With theoptimization.splitChunks configuration option in place, we should now see the duplicate dependency removed from ourindex.bundle.js andanother.bundle.js. The plugin should notice that we've separatedlodash out to a separate chunk and remove the dead weight from our main bundle. However, it's important to note that common dependencies are only extracted into a separate chunk if they meet thesize thresholds specified by webpack.
Let's do annpm run build to see if it worked:
...[webpack-cli] Compilation finishedasset vendors-node_modules_lodash_lodash_js.bundle.js549 KiB[comparedfor emit](id hint: vendors)asset index.bundle.js8.92 KiB[comparedfor emit](name: index)asset another.bundle.js8.8 KiB[comparedfor emit](name: another)Entrypoint index558 KiB= vendors-node_modules_lodash_lodash_js.bundle.js549 KiB index.bundle.js8.92 KiBEntrypoint another558 KiB= vendors-node_modules_lodash_lodash_js.bundle.js549 KiB another.bundle.js8.8 KiBruntime modules7.64 KiB14 modulescacheable modules530 KiB ./src/index.js257 bytes[built][code generated] ./src/another-module.js84 bytes[built][code generated] ./node_modules/lodash/lodash.js530 KiB[built][code generated]webpack5.4.0 compiled successfullyin241 msHere are some other useful plugins and loaders provided by the community for splitting code:
mini-css-extract-plugin: Useful for splitting CSS out from the main application.Two similar techniques are supported by webpack when it comes to dynamic code splitting. The first and recommended approach is to use theimport() syntax that conforms to theECMAScript proposal for dynamic imports. The legacy, webpack-specific approach is to userequire.ensure. Let's try using the first of these two approaches...
import() calls usepromises internally. If you useimport() with older browsers (e.g., IE 11), remember to shimPromise using a polyfill such ases6-promise orpromise-polyfill.
Before we start, let's remove the extraentry andoptimization.splitChunks from our configuration in the above example as they won't be needed for this next demonstration:
webpack.config.js
const path = require('path');module.exports = { mode: 'development', entry: { index: './src/index.js',- another: './src/another-module.js', }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), },- optimization: {- splitChunks: {- chunks: 'all',- },- },};We'll also update our project to remove the now unused files:
project
webpack-demo|- package.json|- package-lock.json|- webpack.config.js|- /dist|- /src |- index.js- |- another-module.js|- /node_modulesNow, instead of statically importinglodash, we'll use dynamic importing to separate a chunk:
src/index.js
-import _ from 'lodash';--function component() {+function getComponent() {- const element = document.createElement('div');- // Lodash, now imported by this script- element.innerHTML = _.join(['Hello', 'webpack'], ' ');+ return import('lodash')+ .then(({ default: _ }) => {+ const element = document.createElement('div');++ element.innerHTML = _.join(['Hello', 'webpack'], ' ');- return element;+ return element;+ })+ .catch((error) => 'An error occurred while loading the component');}-document.body.appendChild(component());+getComponent().then((component) => {+ document.body.appendChild(component);+});The reason we needdefault is that since webpack 4, when importing a CommonJS module, the import will no longer resolve to the value ofmodule.exports, it will instead create an artificial namespace object for the CommonJS module. For more information on the reason behind this, readwebpack 4: import() and CommonJs.
Let's run webpack to seelodash separated out to a separate bundle:
...[webpack-cli] Compilation finishedasset vendors-node_modules_lodash_lodash_js.bundle.js549 KiB[comparedfor emit](id hint: vendors)asset index.bundle.js13.5 KiB[comparedfor emit](name: index)runtime modules7.37 KiB11 modulescacheable modules530 KiB ./src/index.js434 bytes[built][code generated] ./node_modules/lodash/lodash.js530 KiB[built][code generated]webpack5.4.0 compiled successfullyin268 msAsimport() returns a promise, it can be used withasync functions. Here's how it would simplify the code:
src/index.js
-function getComponent() {+async function getComponent() {+ const element = document.createElement('div');+ const { default: _ } = await import('lodash');- return import('lodash')- .then(({ default: _ }) => {- const element = document.createElement('div');+ element.innerHTML = _.join(['Hello', 'webpack'], ' ');- element.innerHTML = _.join(['Hello', 'webpack'], ' ');-- return element;- })- .catch((error) => 'An error occurred while loading the component');+ return element;}getComponent().then((component) => { document.body.appendChild(component);});It is possible to provide adynamic expression toimport() when you might need to import specific module based on a computed variable later.
Webpack 4.6.0+ adds support for prefetching and preloading.
Using these inline directives while declaring your imports allows webpack to output “Resource Hint” which tells the browser that for:
An example of this is having aHomePage component, which renders aLoginButton component which then on demand loads aLoginModal component after being clicked.
LoginButton.js
//...import(/* webpackPrefetch: true */'./path/to/LoginModal.js');This will result in<link rel="prefetch" href="login-modal-chunk.js"> being appended in the head of the page, which will instruct the browser to prefetch in idle time thelogin-modal-chunk.js file.
webpack will add the prefetch hint once the parent chunk has been loaded.
Preload directive has a bunch of differences compared to prefetch:
An example of this can be having aComponent which always depends on a big library that should be in a separate chunk.
Let's imagine a componentChartComponent which needs a hugeChartingLibrary. It displays aLoadingIndicator when rendered and instantly does an on demand import ofChartingLibrary:
ChartComponent.js
//...import(/* webpackPreload: true */'ChartingLibrary');When a page which uses theChartComponent is requested, the charting-library-chunk is also requested via<link rel="preload">. Assuming the page-chunk is smaller and finishes faster, the page will be displayed with aLoadingIndicator, until the already requestedcharting-library-chunk finishes. This will give a little load time boost since it only needs one round-trip instead of two. Especially in high-latency environments.
UsingwebpackPreload incorrectly can actually hurt performance, so be careful when using it.
Sometimes you need to have your own control over preload. For example, preload of any dynamic import can be done via async script. This can be useful in case of streaming server side rendering.
constlazyComp=()=>import('DynamicComponent').catch((error)=>{// Do something with the error.// For example, we can retry the request in case of any net error});If the script loading will fail before webpack starts loading of that script by itself (webpack creates a script tag to load its code, if that script is not on a page), that catch handler won't start tillchunkLoadTimeout is not passed. This behavior can be unexpected. But it's explainable — webpack can not throw any error, cause webpack doesn't know, that script failed. Webpack will add onerror handler to the script right after the error has happen.
To prevent such problem you can add your own onerror handler, which removes the script in case of any error:
<scriptsrc="https://example.com/dist/dynamicComponent.js"asynconerror="this.remove()"></script>In that case, errored script will be removed. Webpack will create its own script and any error will be processed without any timeouts.
Once you start splitting your code, it can be useful to analyze the output to check where modules have ended up. Theofficial analyze tool is a good place to start. There are some other community-supported options out there as well:
SeeLazy Loading for a more concrete example of howimport() can be used in a real application andCaching to learn how to split code more effectively.