Configure code splitting
When bundling code, all JS files will be bundled into one file, resulting in a large file size. If we only need to render the homepage, we should only load the JS file of the homepage, and not load other files. Therefore, we need to code split the files generated by the package to generate multiple JS files. In this way, only the corresponding JS file is loaded for rendering a certain page, reducing loading resources and increasing speed.
Code splitting mainly does two things:
-
Split files: Split the files generated by the package to generate multiple JS files.
-
Load on demand: Load the file you need.
There are three common code splitting methods:
-
Entry starting point: Use
entry
configuration to manually split the code, split the entry file and other files, the entry file is responsible for rendering the first screen content, and other files are responsible for rendering non-first screen content. -
Prevent duplication: Use
entry dependency
orSplitChunksPlugin
to remove duplication and split chunks. -
Dynamic import: Split code by calling inline functions of modules.
Entry starting point-multiple entry configuration
The entry starting point is the most basic method of Webpack to split code. By configuring the entry
property, you can specify one or more entry files, and Webpack will automatically package these files into one file.
In order to ensure the best possible effect of code splitting, the sample code here is as simple as possible.
Project initialization
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// Single entry
// entry: './src/main.js',
// Multiple entry
entry: {
main: './src/main.js',
app: './src/app.js',
},
output: {
path: path.resolve(__dirname, './dist'),
// [name] is the webpack naming rule, using the name of the chunk as the output file name.
// What is a chunk? The packaged resource is a chunk, and the output is called a bundle.
// What is the name of the chunk? For example: in entry, xxx: "./src/xxx.js", name is xxx. Note that it is the xxx in front, which has nothing to do with the file name.
// Why is it necessary to name it like this? If you still write it as main.js, then the two js files generated by packaging will be called main.js and will be overwritten. (Actually, it will report an error directly)
filename: 'js/[name].bundle.js',
clean: true,
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
mode: 'production',
};
export function sum(...args) {
return args.reduce((acc, val) => acc + val, 0);
}
import { sum } from './math';
console.log('Hello from main.js');
console.log(sum(1, 2, 3));
import { sum } from './math';
console.log('Hello from app.js');
console.log(sum(4, 5, 6));
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>code splitting demo 1</title>
</head>
<body></body>
</html>
The files required for the project have been created. Next, install the related dependencies.
First npm init -y
initialize the project, then install webpack, webpack-cli and html-webpack-plugin.
- npm
- Yarn
- pnpm
npm init -y
npm install webpack webpack-cli html-webpack-plugin -D
yarn init -y
yarn add webpack webpack-cli html-webpack-plugin --dev
pnpm init -y
pnpm add webpack webpack-cli html-webpack-plugin -D
The project structure after creating the project is as follows:
├── public
| ├── index.html
├── src
| ├── app.js
| ├── math.js
| └── main.js
├── package.json
├── package-lock.json (This file is automatically generated when npm installs dependencies. If you use other package management tools, it will be different)
└── webpack.config.js
Run the command
npx webpack
After running the command, main.js and app.js files will be generated in the dist folder.
├── dist
| ├── js
| | ├── app.bundle.js
| | └── main.bundle.js
| └── index.html
From the above results, it can be seen that after configuring multiple entry points, there are several output files for each entry point. However, math.js is not configured in the entry point, so it is not packaged into the output file, but a copy of math.js is overwritten in each file that introduces math.js.
When the math.js file becomes larger and larger and is referenced more and more times, the packaged files will grow exponentially and the performance will become lower and lower.
Prevent duplication - SplitChunksPlugin
To solve the above problems, Webpack provides the SplitChunksPlugin
plug-in, which can automatically split the code and prevent duplication.
Modify the configuration file
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// Single entry
// entry: './src/main.js',
// Multiple entry
entry: {
main: './src/main.js',
app: './src/app.js',
},
output: {
path: path.resolve(__dirname, './dist'),
// [name] is the webpack naming rule, using the name of the chunk as the output file name.
// What is a chunk? The packaged resource is a chunk, and the output is called a bundle.
// What is the name of the chunk? For example: in entry xxx: "./src/xxx.js", name is xxx. Note that it is the xxx in front, which has nothing to do with the file name.
// Why is it necessary to name it like this? If you still write it as main.js, then the two js files generated by packaging will be called main.js and will be overwritten. (Actually, an error will be reported directly)
filename: 'js/[name].bundle.js',
clean: true,
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
mode: 'production',
optimization: {
// Code splitting configuration
splitChunks: {
chunks: 'all', // Split all modules
// The following is the default value
// minSize: 20000, // Minimum size of split code
// minRemainingSize: 0, // Similar to minSize, finally ensure that the size of the extracted file cannot be 0
// minChunks: 1, // At least the number of times it is referenced, the code will be split only if the conditions are met
// maxAsyncRequests: 30, // Maximum number of files loaded in parallel when loading on demand
// maxInitialRequests: 30, // Maximum number of parallel requests for entry js files
// enforceSizeThreshold: 50000, // If it exceeds 50kb, it will be packaged separately (minRemainingSize, maxAsyncRequests, maxInitialRequests will be ignored at this time)
// cacheGroups: { // Group, which modules should be packaged into one group
// defaultVendors: { // Group name
// test: /[\\/]node_modules[\\/]/, // Modules that need to be packaged together
// priority: -10, // Weight (the larger the weight, the higher)
// reuseExistingChunk: true, // If the current chunk contains a module that has been split from the main bundle, it will be reused instead of generating a new module
// },
// default: { // Other configurations not written will use the default values above
// minChunks: 2, // minChunks has a greater weight here
// priority: -20,
// reuseExistingChunk: true,
// },
// },
// Modify configuration
cacheGroups: {
// Group, which modules should be packaged into one group
// defaultVendors: { // Group name
// test: /[\\/]node_modules[\\/]/, // Modules that need to be packaged together
// priority: -10, // Weight (the larger the higher)
// reuseExistingChunk: true, // If the current chunk contains a module that has been split from the main bundle, it will be reused instead of generating a new module
// },
default: {
//Other configurations that are not written will use the above default values.
minSize: 0, // The file size we defined is too small, so we need to change the minimum file size for packaging
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
Run command
npx webpack
The packaged file directory is as follows:
├── dist
| ├── js
| | ├── app.bundle.js
| | ├── main.bundle.js
| | └── 456.bundle.js (new file, the name is the name of the generated chunk, which corresponds to the math.js before packaging)
| └── index.html
Observing the content of the file, we can find that math.js is split into a separate file and has not been overwritten into main.js and app.js.
Dynamic import-import
Webpack also provides the function of dynamic import, which can load modules on demand, thereby achieving code splitting to significantly improve the loading speed of certain pages.
In order to reflect the effect of dynamic import, we slightly add a simple button click function to the project.
export function count(a, b) {
return a - b;
}
import { sum } from './math';
import { count } from './count';
console.log('Hello from main.js');
console.log(sum(1, 2, 3));
document.getElementById('btn').onclick = () => {
console.log(count(8, 6));
};
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>code splitting demo 2</title>
</head>
<body>
<button id="btn">count</button>
</body>
</html>
At this time, package and observe the effect before the dynamic import of the configuration:
npx webpack
You can currently see that count.js is packaged into main.buldle.js because main.buldle.js calls count.js.
As shown in the figure above, the three files app.bundle.js, main.bundle.js, and 456.bundle.js were all loaded at the same time when running for the first time, but we did not click the button to trigger the count function.
Next we use dynamic import to load the module on demand.
Modify file code
import { sum } from './math';
// import { count } from './count';
console.log('Hello from main.js');
console.log(sum(1, 2, 3));
document.getElementById('btn').onclick = function () {
// Dynamic import --> achieve on-demand loading
// Even if it is only referenced once, the code will be split
import('./count.js').then(({ count }) => {
console.log(count(8, 6));
});
};
Run command
npx webpack
At this point you can see that count.bundle.js is split into a separate file.
At this point, click the button and you can see that the count function is loaded and executed on demand.
Single entry configuration
Because we may adopt the SPA (Single Page Application) mode during development, that is, there is only one entry file.
Modify files
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// single entry
entry: './src/main.js',
//Multiple entries
// entry: {
// main: './src/main.js',
// app: './src/app.js',
// },
output: {
path: path.resolve(__dirname, './dist'),
// [name] is the webpack naming rule, using the name of the chunk as the output file name.
// What is chunk? The packaged resources are chunks, and the output is called bundle.
//What is the name of the chunk? For example: xxx in entry: "./src/xxx.js", the name is xxx. Note that the xxx in front has nothing to do with the file name.
// Why do we need to name it this way? If you still write main.js as before, then the two js files generated by packaging will be called main.js and overwriting will occur. (In fact, an error will be reported directly)
filename: 'js/[name].bundle.js',
clean: true,
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
mode: 'production',
optimization: {
// Code splitting configuration
splitChunks: {
chunks: 'all', // Split all modules
//The following are the default values
// minSize: 20000, // Minimum size for split code
// minRemainingSize: 0, // Similar to minSize, finally ensure that the extracted file size cannot be 0
// minChunks: 1, // At least the number of times it is referenced, the code will be split only when the conditions are met
// maxAsyncRequests: 30, // The maximum number of files loaded in parallel when loading on demand
// maxInitialRequests: 30, // The maximum number of parallel requests for the entry js file
// enforceSizeThreshold: 50000, // Anything over 50kb will be packaged separately (minRemainingSize, maxAsyncRequests, maxInitialRequests will be ignored at this time)
// cacheGroups: { // Group, which modules should be packaged into a group
// defaultVendors: { // Group name
// test: /[\\/]node_modules[\\/]/, // Modules that need to be packaged together
// priority: -10, // Weight (the bigger, the higher)
// reuseExistingChunk: true, // If the current chunk contains a module that has been split from the main bundle, it will be reused instead of generating a new module
// },
// default: { // Other configurations not written will use the above default value
// minChunks: 2, // The minChunks here have greater weight
// priority: -20,
// reuseExistingChunk: true,
// },
// },
// Change setting
cacheGroups: {
//Group, which modules should be packaged into a group
// defaultVendors: { // Group name
// test: /[\\/]node_modules[\\/]/, // Modules that need to be packaged together
// priority: -10, // Weight (the bigger, the higher)
// reuseExistingChunk: true, // If the current chunk contains a module that has been split from the main bundle, it will be reused instead of generating a new module
// },
default: {
//Other configurations not written will use the above default value
minSize: 0, // The file size we defined is too small, so we need to change the minimum file size for packaging
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
In order to reflect the single-entry effect and code splitting of node_modules, we introduced a third-party library dayjs into the project.
- npm
- Yarn
- pnpm
npm install dayjs -S
yarn add dayjs
pnpm add dayjs
import { sum } from './math';
console.log('Hello from main.js');
console.log(sum(1, 2, 3));
document.getElementById('btn').onclick = function () {
// Dynamic import --> On-demand loading
// Even if it is only referenced once, the code will be split
import('./count.js').then(({ count }) => {
console.log(count(8, 6));
});
// Introduce third-party libraries --> Code splitting
import('dayjs').then(({ default: dayjs }) => {
console.log(dayjs().format('YYYY-MM-DD HH:mm:ss'));
});
};
Run command
npx webpack
At this point, you can see that dayjs is split into a separate file.
As can be seen from the above figure, by clicking the button, you can see that the count function and dayjs are loaded and executed on demand.
We have split the code and used the import
dynamic import syntax to implement on-demand loading (i.e. lazy loading, such as route lazy loading).
However, the loading speed is still not ideal. For example, when the user clicks the button, the resource is loaded. If the resource size is large, the user will feel a noticeable lag.
To solve this problem, we hope to load the resources required later during the browser's idle time. Therefore, we need to use Preload
or Prefetch
technology.
preload and prefetch configuration
First, let's introduce the similarities and differences between these two technologies.
Their common points:
-
Only load resources, not execute.
-
Has cache function.
Their differences:
-
Preload
has a high loading priority, whilePrefetch
has a low loading priority. -
Preload
can only load resources required by the current page, whilePrefetch
can load resources for both the current page and the next page.
Summary:
-
Resources with high priority on the current page are loaded using
Preload
. -
Resources required for the next page are loaded using
Prefetch
.
Their problems: poor compatibility.
-
We can query API compatibility issues through the Can I Use website.
-
In comparison,
Preload
has better compatibility thanPrefetch
.
Next, let's implement the configuration of Preload
and Prefetch
.
Modify the file
import { sum } from './math';
console.log('Hello from main.js');
console.log(sum(1, 2, 3));
document.getElementById('btn').onclick = function () {
// Dynamic import --> Implement on-demand loading
// Even if it is only referenced once, the code will be split
// highligh-next-line
import(/* webpackPrefetch: true */ './count.js').then(({ count }) => {
console.log(count(8, 6));
});
// highligh-next-line
import(/* webpackPreload: true */ 'dayjs').then(({ default: dayjs }) => {
console.log(dayjs().format('YYYY-MM-DD HH:mm:ss'));
});
};
webpack5 adds webpackPreload
and webpackPrefetch
comments to configure resource loading strategies.
Run command
npx webpack
As can be seen from the above figure, we added prefecth to count.js, so count.js will be loaded when the page is loaded, and when the button is actually clicked, no request will be made, but it will be loaded directly from the cache, thereby improving the response speed when clicking.
Using webpackPreload incorrectly will damage performance, please use it with caution.