Webpack ~ Code Splitting - rohit120582sharma/Documentation GitHub Wiki

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 two general approaches to code splitting available:

  • Entry Points:
    • Manually split code using entry configuration
    • Use the SplitChunksPlugin to dedupe and split chunks
  • Dynamic Imports:
    • Split code via inline function calls within modules


Entry Points

This is by far the easiest and most intuitive way to split code. However, it is more manual and as mentioned there are some pitfalls to this approach:

  • If there are any duplicated modules between entry chunks they will be included in both bundles.
  • It isn't as flexible and can't be used to dynamically split code with the core application logic.

The SplitChunksPlugin allows us to extract common dependencies into an existing entry chunk or an entirely new chunk. It can be configured via optimization.splitChunks.

The SplitChunksPlugin also has some great properties:

  • It never downloads unneeded module (as long you don’t enforce chunk merging via name)
  • It’s on by default for async chunks
  • It handles vendor splitting with multiple vendor chunks

As we know, the SplitChunksPlugin can be used to split modules out into separate bundles. Webpack provides an optimization feature to split runtime code into a separate chunk using the optimization.runtimeChunk option. Set it to single to create a single runtime bundle for all chunks.

It's also good practice to extract third-party libraries, such as lodash or react, to a separate vendor chunk as they are less likely to change than our local source code. This step will allow clients to request even less from the server to stay up to date. This can be done by using the cacheGroups option.

Also good to have optimization.moduleIds with hashed option to maintain the same hash and not updating the chunks filename if there is no change in their content.

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
	entry: {
		index: './src/index.js',
		another: './src/another-module.js'
	},
	output: {
		filename: '[name].[contenthash].bundle.js',
		path: path.resolve(__dirname, 'dist')
	},
	optimization: {
		moduleIds: 'hashed',
		runtimeChunk: 'single',
		splitChunks: {
			cacheGroups: {
				vendor: {
					test: /[\\/]node_modules[\\/]/,
					name: 'vendors',
					chunks: 'all'
				}
			}
		}
	},
	plugins: [
		new CleanWebpackPlugin(),
		new HtmlWebpackPlugin({
			title: 'Caching'
		})
	]
}


Dynamic Imports / Lazy Loading

Lazy, or "on demand", loading is a great way to optimize your site or application. This practice essentially involves splitting your code at logical breakpoints, and then loading it once the user has done something that requires, or will require, a new block of code. This speeds up the initial load of the application and lightens its overall weight as some blocks may never even be loaded.

The approach is to use the import() function to load modules dynamically. Calls to import() are treated as split points, meaning the requested module and its children are split out into a separate chunk.

Note output.filename option does not affect output files for on-demand-loaded chunks. For these files the output.chunkFilename option is used which determines the name of non-entry chunk files.

Now, instead of statically importing lodash, we'll use dynamic importing to separate a chunk. Note the use of webpackChunkName in the comment. This will cause our separate bundle to be named lodash.bundle.js instead of just [id].bundle.js.

webpack.config.js

module.exports = {
	output: {
		filename: '[name].[contenthash].bundle.js',
		chunkFilename: '[name].chunk-bundle.js',
		path: path.resolve(__dirname, 'dist')
	}
}

src/index.js

function getComponent() {
	return import(/* webpackChunkName: "lodash" */ 'lodash')
		.then(({ default: _ }) => {
			const element = document.createElement('div');
			element.innerHTML = _.join(['Hello', 'webpack'], ' ');
			return element;
		})
		.catch(error => 'An error occurred while loading the component');
}
getComponent().then(component => {
	document.body.appendChild(component);
});

Dynamic expressions in import()

It is not possible to use a fully dynamic import statement, such as import(foo).

The import() must contain at least some information about where the module is located. Bundling can be limited to a specific directory or set of files so that when you are using a dynamic expression - every module that could potentially be requested on an import() call is included.

For example, import('./locale/${language}.json') will cause every .json file in the ./locale directory to be bundled into the new chunk. At run time, when the variable language has been computed, any file like english.json or german.json will be available for consumption.

import(`./locale/${language}.json`).then(module => {
	// do something with the translations
});

Magic Comments

By adding comments to the import, we can do things such as name our chunk or select different modes. Inline comments to make features work.

import(
	/* webpackInclude: /\.json$/ */
	/* webpackExclude: /\.noimport\.json$/ */
	/* webpackChunkName: "my-chunk-name" */
	/* webpackMode: "lazy" */
	/* webpackPrefetch: true */
	/* webpackPreload: true */
	`./locale/${language}`
);
  • webpackChunkName : A name for the new chunk. Adding this comment will cause our separate chunk to be named [my-chunk-name].js instead of [id].js.
  • webpackMode
    • "lazy" : generates a lazy-loadable chunk for each imported module.
    • "lazy-once" : this only makes sense in the case of a partially dynamic statement, e.g. import(./locales/${language}.json), where there are multiple module paths that could potentially be requested.

Prefetching/Preloading modules

Using these inline directives while declaring your imports allows webpack to output “Resource Hint” which tells the browser that for:

  • prefetch : resource is probably needed for some navigation in the future. This will result in <link rel="prefetch" href="[name].chunk-bundle.js"> being appended in the head of the page, which will instruct the browser to prefetch in idle time.
  • preload : resource might be needed during the current navigation.

Preload directive has a bunch of differences compared to prefetch:

  • A preloaded chunk starts loading in parallel to the parent chunk. A prefetched chunk starts after the parent chunk finishes loading.
  • A preloaded chunk has medium priority and is instantly downloaded. A prefetched chunk is downloaded while the browser is idle.
  • A preloaded chunk should be instantly requested by the parent chunk. A prefetched chunk can be used anytime in the future.
⚠️ **GitHub.com Fallback** ⚠️