Project optimization solution in Webpack scenario
In a front-end project based on webpack as a build tool, there are usually two aspects to optimize.
- [Compilation and build time optimization] (#Compilation and build time optimization)
- [Build product optimization] (#Build product optimization)
Compilation and build time optimization
Compile build time optimization, aiming to speed up each build and reduce build time. It includes the time overhead for each modification file recompilation during development; the overall time overhead for building the final product for the project.
Optimization directions include:
- [Compilation and build time optimization] (#Compilation and build time optimization).
- [Reduce file matching range] (#Reduce file matching range).
- [File suffix matching] (#File suffix matching).
- Cache.
- [Parallel build] (#parallel build).
Reduce file matching range
When configuring webpack loader, two properties are usually specified: test
and use
to declare which files need to be converted.
module.exports = {
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
}
By default, the matching lookup range is searching relative to the context of the project root directory, which can be very time-consuming when the project files are high. In this case, you can use the include
and exclude
properties to limit the file matching range.
const path = require('node:path')
module.exports = {
module: {
rules: [
{
test: /\.txt$/,
use: 'raw-loader',
include: path.resolve(__dirname, 'src'),
exclude: /node_modules/,
},
],
},
}
exclude
: Exclude all qualified filesinclude
: Only all qualified files
Reasonable use of the include
and exclude
attributes can effectively reduce the file matching range, thereby reducing build time.
**Reference: ** webpack module.rules
File suffix matching
Usually when we import modules, we are accustomed to ignoring the file suffix name because webpack
will help complete.
But this comes at a cost. Webpack internally tries to use built-in configuration, complete the suffix and find the file exists, and then try to load it until the file matches. This can incur additional I/O overhead.
On the one hand, you can modify the webpack configuration resolve.extensions
, adjust the suffix completion rules, and control the priority of completion through sequence. Put the most commonly used file suffixes at the top and reduce non-essential suffix names.
module.exports = {
resolve: {
// Do not write configuration if not necessary.
extensions: ['.tsx', '.ts', '.js'],
},
}
On the other hand, when importing modules, try not to ignore the file suffix name.
Reference: webpack resolve.extensions
cache
Every time you start the build, if you need to recompile all files, it will inevitably take a long time. Therefore, the compilation results need to be cached so that the cached results will be loaded directly next time and only the modified files will be recompiled.
In webpack5
, a cache
configuration is provided to enable cache directly.
module.exports = {
cache: {
type: 'filesystem',
},
}
Reference: webpack cache
Parallel construction
webpack runs in a NodeJS environment and is single-threaded, so it can only do one thing at a time. Currently, mainstream computers are multi-core, and this feature can be used to enable webpack to be built in parallel. Typically, use thread-loader to implement parallel construction.
module.exports = {
module: {
rules: [
{
test: /.jsx?$/,
use: [
// Enable multi-process packaging.
{
loader: 'thread-loader',
options: {
workers: 3, // 3 processes are opened
},
},
{ loader: 'babel-loader' },
],
},
],
},
}
The loader placed after the thread-loader will run in a separate worker pool. Each worker is a separate node.js process with a limit of 600ms. At the same time, data exchange across processes will also be restricted. So it is recommended to use it only on time-consuming loaders.
If the project is not large and there are not many files, there is no need to use thread-loader. It also has additional performance overhead.
Build product optimization
The purpose of building product optimization is to reduce the volume of the build product and organize the build product reasonably, thereby improving the loading speed of the page, the loading speed of the first screen, etc.
General build optimizations include:
- [Compress
js
,css
,html
code](#Compress -js-csshtml-code). - [Compressed Image Resource] (#Compressed Image Resource).
- [Code segmentation] (#Code segmentation).
- [Load on demand] (#Load on demand).
- preload, prefetch.
- tree-shaking.
Compress js, css, html code
Compress js
Use terser-webpack-plugin
to compress js code:
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
},
}
Compress css
Compress the css code through css-minimizer-webpack-plugin
, Use mini-css-extract-plugin
to extract css into a separate file.
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
// Extract into separate files
MiniCssExtractPlugin.loader,
'css-loader',
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
// Define the output file name and directory
filename: 'asset/css/style.css',
}),
],
optimization: {
minimize: true,
minimizer: [
// Compress css
new CssMinimizerPlugin({}),
],
},
}
Compress html
Use html-webpack-plugin
to compress html code.
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins: [
new HtmlWebpackPlugin({
// Dynamically generate html files
template: './index.html',
minify: {
// Compress HTML
removeComments: true, // Remove comments in HTML
collapseWhitespace: true, // Remove space and line breaks
minifyCSS: true, // compress inline css
},
}),
],
}
Compress image resources
There are many ways to compress image resources and you need to choose according to actual conditions. It also includes processing of multiplex graphs, such as: @2x
, @3x
, @4x
, etc.
For example, you can use image-webpack-loader
to achieve image compression.
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif|jpeg|webp|svg)$/,
use: [
'file-loader',
{
loader: 'image-webpack-loader',
options: {
mozjpeg: { progressive: true },
optipng: { enabled: false },
pngquant: { quality: [0.65, 0.9], speed: 4 },
gifsicle: { interlaced: false },
},
},
],
exclude: /node_modules/,
},
],
},
}
Code segmentation
If it is a medium-sized project, or an MPA project, it will generally have multiple pages. But they all use the same technology stack and have reused public resources. If the code of each page contains these same codes alone, it will lead to waste of resources. Each time a different page is loaded, duplicate resources will be loaded. Waste of user traffic, slow page loading, affecting user experience.
In this case, the third-party modules and public resources are split into independent files separately. Using the caching mechanism, different pages only need to spend the first load time when loading, reducing the waiting time for secondary loading.
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async', // The values are `all`, `async` and `initial`
minSize: 20000, // Generate the minimum volume of the chunk (in bytes).
minRemainingSize: 0,
minChunks: 1, // The minimum number of chunks that must be shared before splitting.
maxAsyncRequests: 30, // Maximum number of parallel requests when loading on demand.
maxInitialRequests: 30, // Maximum number of parallel requests for entry point.
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\/]node_modules[\/]/, //The third-party module is removed
priority: -10,
reuseExistingChunk: true,
},
utilVendors: {
test: /[\/]utils[\/]/, //Disassemble the public module
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
}
Reference: Code Separation
Load on demand
Most of the time, making the page available does not require loading all resources.
For example, in SPA/MPA applications, different pages are implemented through routing. If all page codes are packaged in the same file, Then when loading the code for a certain page, the code for other pages is actually loaded, resulting in the loading speed of the page not meeting expectations.
Because splitting the resources of the routing page into different files and loading these resources only when used can reduce the loading time of the current page.
const List = lazyComponent('list', () => import(/* webpackChunkName: "list" */ '@/pages/list'))
const Detail = lazyComponent('detail', () => import(/* webpackChunkName: "detail" */ '@/pages/detail'))
Furthermore, the faster the page is rendered, the more conducive it is to the user experience. Therefore, you can also analyze the key resources required to complete the first-screen rendering of the current page, split the non-critical resources, and only load it for the first time Critical resources, and then load non-critical resources after completion.
preload, prefetch
- refetch: Resources that may be required under certain navigation in the future
- preload (preload): Resources may be required under the current navigation
Use prefetch
in webpack to implement prefetch:
// ...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js')
This generates <link rel="prefetch" href="login-modal-chunk.js">
and appends to the page header, instructing the browser to prefetch the login-modal-chunk.js file at idle time.
Use preload
in webpack to implement preload:
//...
import(/* webpackPreload: true */ 'ChartingLibrary')
When using ChartComponent
in the page, while requesting ChartComponent.js
, charting-library-chunk
will also be requested via <link rel="preload">
. Assume that the page-chunk
volume is smaller and faster than the charting-library-chunk
, The page will now display LoadingIndicator
, wait until the charting-library-chunk
request is completed. The LoadingIndicator
component just disappeared. This will allow for a shorter loading time, as only a single round trip is performed, Instead of two round trips, especially in high latency environments.
Reference: webpack prefetch/preload
tree-shaking
tree shaking
is turned on by default in production mode
What should be noted is:
- Effective only for ESM
- ES6 modules that can only be statically declared and referenced, and cannot be dynamically introduced and declared.
- Only handle module level, but not function level redundancy.
- Only JS-related redundant code can be processed, but not CSS redundant code.
For CSS resources, you can use the purgecss-webpack-plugin
plugin to tree-shaking on CSS.
const PurgecssPlugin = require('purgecss-webpack-plugin')
module.exports = {
plugins: [
new PurgeCSSPlugin({
paths: glob.sync('src/**/*', { nodir: true }),
}),
],
}
Reference: webpack tree-shaking