Ant Design Pro scaffolding was chosen for the front end of the company’s project. However, Ant Design Pro has switched to UMI framework, which was originally built based on version 2.0. It is slightly more difficult to upgrade the project. However, with the accumulation of bricks in the past few years, the project has grown from 2MB originally packaged to 10+MB now, and it is deployed on Tomcat without CDN or GZIP. The first screen is getting slower and slower to load, and customers are complaining more and more.
As a result, I could not help spending several days to do a thorough optimization.
The first step is to familiarize yourself with packaging optimizations
After reading the article https://webpack.wuhaolin.cn/ benefit, it is recommended that no webpack optimization experience we can carefully reading children’s shoes. At the same time, we can refer to it when reforming our own projects.
To summarize, common optimization strategies for WebPack are as follows:
- using
BundleAnalyzerPlugin
Analyze dependent packages - open
gzip
Compression, this requires the server to do configuration can be reduced50%
Left and right package volume - Separate the partially referenced external dependent libraries to
CDN
Static resource import mode - Package filtering
moment
The language pack - Separate dependency packages (can be the whole
node_modules
) tovendor.js
file
In addition, improving packaging efficiency is also a necessary thing. In both development and production environments, saving as much time as possible to wait for packaging will improve our productivity. Especially during development and hot updates, packaging compilation speed can make a big difference. Common strategies for packaging efficiency are as follows:
- Enabling multi-threaded packaging
- The filtering part is unnecessary and time-consuming
webpack
The plug-in - Upgrade the code compression plug-in and enable caching
- Different configurations for development and production
webpack
The plug-in - using
SpeedMeasurePlugin
Plug-ins analyze time-consuming packaging
Step 2, create a customwebpack
configuration
Ant Design Pro uses Roadhog framework Github by default. Roadhog itself encapsulates webpack-related capabilities and provides developers with a lot of traversal configuration items. Synchronization of these configurations allows us to achieve functions such as local proxy forwarding and subcontract loading. But these configuration items are not flexible enough, and to do our optimizations well, we need to configure our WebPack in a different way.
What did Roadhog do for us? View the source code, you can clearly see what it does: github.com/umijs/umi/b…
self-builtwebpack
The configuration file
In the root directory, create the file webpack.config.js, which is the configuration file that WebPack reads by default. The content is as follows:
export default webpackConfig => {
return webpackConfig;
}
Copy the code
Here we export a function that takes the existing WebPack configuration from the Roadhog configuration information, so we can modify the existing WebPack configuration information in our configuration item. Ignore warnings from the console at compile time:
At this point, we have the portal to modify our WebPack packaging. Readers can print their own log in the function to see the corresponding output.
Adding a Monitoring Plug-in
The BundleAnalyzerPlugin and SpeedMeasurePlugin plug-ins are typically used to analyze the packaging volume and packaging time consumed. The usage of both is also relatively simple, as follows:
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
export default webpackConfig => {
// Package resource analysis
webpackConfig.plugins.push(
new BundleAnalyzerPlugin()
);
// Package speed monitor
return smp.wrap(webpackConfig);
}
Copy the code
- added
BundleAnalyzerPlugin
After the plug-in is packaged, it will automatically open immediately127.0.0.0:8888
Web page that shows the size of dependencies and source files for the entire project. You can use this to determine the size of dependencies and whether there are duplicate dependencies (duplicate dependencies can be found in thepackage.json
The unified declaration of solution). (TIPS:roadhog
The plug-in itself is provided and only needs to be configured in the package commandANALYZE=true
Environment variables, such as:cross-env ANALYZE=true roadhog build
In order to enhance the control over the use of the plug-in, I reconfigure it in my own configuration.
http://127.0.0.0:8888
As you can see, the size of the files in the node_modules directory accounts for almost half of the total packaging volume. At the same time, several dependent libraries are relatively large, such as ANTD, XLSX, BizCharts and so on.
- added
SpeedMeasurePlugin
After the plug-in is packaged, the console will output the following information:
As shown in the figure, some plug-ins, such as CaseSensitivePathsPlugin and IgnorePlugin, take more than one minute. Style-loader and babel-loader also take a lot of time. These are all items that we can optimize in a custom configuration.
The third step, [optimization] reduce the packaging volume
Configure webpack according to the methods mentioned in the beginning to compress the packing volume.
1. Opengzip
The compression
Modern browsers by default support the resolution of Gzip compressed JS files, which consumes a certain amount of decompression time compared to uncompressed JS files. However, compared with network delay, the advantages brought by compression and decompression are generally greater than network delay, especially in the weak network environment.
In our project, SpringBoot + Tomcat is used as the server for static resources. Therefore, here’s how to configure Gzip in SpringBoot. And find the resources/application. The properties file, add the following code:
# gzip compression
server.compression.enabled=true
server.compression.mime-types=application/javascript,text/css,application/xml,text/html,text/xml,text/plain
Copy the code
Starting gzip under SpringBoot is as simple as that. The content-encoding of Response Headers can be seen by opening the page in the browser and looking at any of our JS files or CSS files on the console-web. Gzip indicates that gzip has been enabled and the file size is much smaller.
Other servers under the Gzip configuration mode can be Google, the general operation is relatively simple.
We can further optimize on the server: onCache Control
We continue to resources/application. The properties in the allocation of resources the cache:
# 7 days cache
spring.resources.cache.cachecontrol.max-age=604800
spring.resources.cache.cachecontrol.no-cache=false
spring.resources.cache.cachecontrol.no-store=false
spring.resources.cache.cachecontrol.cache-private=true
Copy the code
After the configuration is complete, the web page will not be cached when it is loaded for the first time, but the web page will be cached when it is refreshed within seven days. As long as the file name stays the same.
Configure cache-control for other servers by Google.
2. Change some dependent packagesCDN
Way to introduce
Why use CDN to introduce dependency packages? I think there are several reasons:
- (1)
CDN
By using common resources and services, you can reduce the size of the package and reduce access and traffic to your own servers - (2)
CDN
You can do a public cache, based on the network of visitors to read data, access faster - (3)
CDN
It will not be affected by project release and has better optimization in browser caching for visitors - (4)
CDN
Similar to subcontracting, content can be loaded in parallel when opening a user
Although there are many benefits, it does not mean that all the packages we rely on need to be loaded statically with the CDN. This is related to the browser’s limit on the number of concurrent requests. Different browsers have different limits on the number of resources requested at the same time:
This means that although many dependent libraries can be introduced through the CDN, they will eventually be loaded in batches serial. Also, the network overhead of each HTTP connection can have a significant impact. Therefore, try to achieve a balance between CDN resources and subcontracting. The treatment of subcontracting will be discussed later. Therefore, it is suggested that students keep the following principles when introducing CDN:
- If possible, choose a bag with a large footprint
- The quantity is guaranteed to be no more than 5
- Working with subcontracting splits the packaging volume to take full advantage of the browser’s parallelism capabilities
- Conditional can be used dynamically
CDN
Domain names break the parallel limits of browsers
So, how do we configure it? Here we have two ways:
- (1) Configuration
.webpackrc.js
theexternals
And the manual inindex.ejs
In the configuration<script... />
- (2) Use plug-ins
HtmlWebpackExternalsPlugin
Of course, it’s all the same. Here we introduce the second approach, which is simpler and easier to configure and maintain. For example, we introduced React, React – DOM, and jQuery as CDN resources.
Add “html-webpack-externals-plugin”: “^3.8.0” to devDependencies. Add “html-webpack-externals-plugin” to devDependencies. Then add the plugin’s configuration to webpack.config.js:
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin')
// ...
export default webpackConfig => {
// ...
// Import external CDN resources
webpackConfig.plugins.push(
new HtmlWebpackExternalsPlugin({
externals: [{module: 'react'.entry: isDev ?
'https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.development.js' :
'https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js'.global: 'window.React'
},
{
module: 'react-dom'.entry: 'https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js'.global: 'window.ReactDOM'
},
{
module: 'jquery'.entry: 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js'.global: 'window.jQuery'}].files: ['index.html']}));// Package speed monitor
return smp.wrap(webpackConfig);
}
Copy the code
Refer to readme for instructions on this plug-in. We can also add any other dependent packages, even common components in the project, etc.
3. Filter in the production environmentmoment
The language pack
(If the project is not using the moment library, please skip it.)
As you can see from the BundleAnalyzerPlugin’s package volume view, moment takes up a lot of volume, and the language package in moment takes up more than half of the space. However, we only use Chinese and English in our projects. So the language pack optimization for Moment is one of the optimizations we can make. The ignoreMomentLocale parameter in roadhog’s configuration is the language package that filters the moment when it is packaged. The effect is achieved when the parameter is set to true.
If we wanted to do it ourselves, how would we do it? Here we’ll use the IgnorePlugin plug-in, which comes with the WebPack itself. It is simple to use, but the plug-in takes a lot of time, which affects the overall packaging time. Therefore, it is recommended that this be enabled during build production packages.
Configuration Reference:
// ...
export default webpackConfig => {
// ...
// Ignore the moment language package
if(! isDev) { webpackConfig.plugins.push(new webpack.IgnorePlugin(/^\.\/locale$/./moment$/));
}
// Package speed monitor
return smp.wrap(webpackConfig);
}
Copy the code
Separation of 4.node_modules
And business code
When it comes to front-end packaging optimization, there are a bunch of tutorials on how to package multiple chunks. Today, we introduce a simple but effective way to package business code and dependency packages separately. The node_modules code is packaged as vendor.js, and the business code is packaged as index.js. This way, all of our dependencies are packaged in Vendor.js, and the browser can cache this file well.
We will also use the CommonsChunkPlugin for Webpack. However, the processing of chunk files requires us to write functions to implement:
// ...
export default webpackConfig => {
// ...
// Separate the packages under node_modules into vendor.hash8.js
const nodeModulesPath = path.join(__dirname, './node_modules');
webpackConfig.plugins.push(new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'.filename: '[name].[hash:8].js'.minChunks: function (module, _) {
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(nodeModulesPath) >= 0)}}));// Package speed monitor
return smp.wrap(webpackConfig);
}
Copy the code
Simply speaking, all JS files under node_modules are bundled into vendor’s chunk file. Of course, CSS-related style files are handled by loader and not by chunk packaging.
As a result, our package will generate two JS files: index.hash8.js and vendor.hash8.js. The plugin automatically adds these two artifacts to index.html.
Of course, if the detached package is still large, you can consider configuring multiple chunk files.
This is the optimization of packaging volume, the final result of our projectindex.js
The size is about that before optimization10%
, the optimization effect is obvious.
Step 4: [optimization] Improve packaging efficiency
1. Filter unnecessary plug-ins
The CaseSensitivePathsPlugin and IgnorePlugin plugins, among others, are obviously time-consuming from the analysis of the packaging time we configured initially. But these plug-ins by themselves have little impact on package size, or in a development environment. Therefore, we can filter out unwanted plug-ins from the Roadhog configuration in the Webpack configuration.
// ...
export default webpackConfig => {
// Filter some useless plug-ins
webpackConfig.plugins = webpackConfig.plugins.filter(p= > {
switch (p.constructor.name) {
case 'UglifyJsPlugin':
case 'CaseSensitivePathsPlugin':
case 'IgnorePlugin':
case 'ProgressPlugin':
return false;
case 'HardSourceWebpackPlugin':
return isDev;
}
return true;
});
/ /...
return smp.wrap(webpackConfig);
}
Copy the code
Here, I’ve filtered a few plug-ins that have little impact on project packaging. The UglifyJsPlugin plug-in has a significant impact on packaging but takes a long time, so ParallelUglifyPlugin is used to replace it.
2. The upgradeUglifyJsPlugin
forParallelUglifyPlugin
readme
This plug-in improves the construction efficiency of UglifyJsPlugin plug-in, and its function is consistent with UglifyJsPlugin. Are used to do JS code compression beautification. This plug-in can effectively reduce the size of the JS package built. It takes a lot of time, but does not affect the packaging operation. Therefore, to further improve the packaging efficiency during development, you can only enable the plug-in in the production environment. The configuration is as follows:
// ...
export default webpackConfig => {
// Production environment
if(! isDev) {// code compression beautification plug-in
webpackConfig.plugins.push(
new ParallelUglifyPlugin({
// The argument passed to UglifyJS
uglifyJS: {
output: {
// Most compact output
beautify: false.// Delete all comments
comments: false,},compress: {
// Delete all 'console' statements, compatible with Internet Explorer
drop_console: true.// Inline variables that are defined but used only once
collapse_vars: true.// Extract static values that occur multiple times but are not defined as variables to reference
reduce_vars: true.// Not in use
unused: false}},cacheDir: './cache'.workerCount: os.cpus().length
}),
);
}
/ /...
return smp.wrap(webpackConfig);
}
Copy the code
3. Use multithreadingbabel-loader
Multi-threaded packaging under Webpack can optimize the efficiency of Loader construction under large projects. Since webPack packaging is mostly dealing with JS files and CSS files, various Loaders are needed to handle it. There are two ways to load a Webpack multithreaded loader: Happypack plug-in and Thread-loader. The performance of the two schemes is similar. However, Happypack has version conflicts on Roadhog’s project, and the configuration is relatively more complex than thread-Loader. Therefore, thread-loader is used here.
The downside of using thread-loader is that it doesn’t support CSS loaders very well. There are compatibility issues with the loader to modify roadhog CSS, so we will only deal with js babel-loader here. The reference configuration is as follows:
// ...
export default webpackConfig => {
webpackConfig.module.rules.forEach(r= > {
switch (r.test + ' ') {
case '/\\.js$/':
case '/\\.jsx$/':
if (r.use && Array.from(r.use).indexOf('thread-loader') < 0) {
r.use.splice(0.0.'thread-loader');
r.exclude = /node_modules/;
}
break;
default:
break; }});/ /...
return smp.wrap(webpackConfig);
}
Copy the code
4. Distinguish between production and development environments
The development environment needs to make packaging compilations (every hot update) as fast as possible, while the console outputs more useful information for developers to debug. Production environments need to keep package sizes as low as possible. In fact, the environmental judgment flag has been used above, and the way to obtain it is also very simple:
// We can determine the current packaging environment by reading the variable proccess.env.
const isDev = process.env.NODE_ENV === 'development';
Copy the code
The following plug-ins are recommended for use in the DEV environment:
BundleAnalyzerPlugin
HardSourceWebpackPlugin
It is recommended that the following plug-ins be used in proD:
ParallelUglifyPlugin
IgnorePlugin
After configuration, the overall packaging efficiency increased by more than 50% in our project.
Finally, the complete configuration file in the project is attached for your reference
const os = require('os');
const webpack = require('webpack');
const path = require('path');
// Extract the public package
const CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin;
// Whether it is a test environment
const isDev = process.env.NODE_ENV === 'development';
// External resources (CDN) plug-in
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin')
// Code compression
constParallelUglifyPlugin = ! isDev ?require('webpack-parallel-uglify-plugin') : null;
// Multithreading concurrency
const threadLoader = require('thread-loader');
threadLoader.warmup({
// pool options, like passed to loader options
// must match loader options to boot the correct pool},// modules to load
// can be any module, i. e.
'babel-loader'.'url-loader'.'file-loader'
]);
// Build a speedometer
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
/ / the progress bar
const WebpackBar = require('webpackbar');
// Resource analysis
const BundleAnalyzerPlugin = isDev ? require('webpack-bundle-analyzer').BundleAnalyzerPlugin : null;
/** * Export the latest webpack configuration, overwriting the configuration exported by Roadhog@param webpackConfig
* @returns {*|(function(... [*]) : *)}
*
* @author luodong* /
export default webpackConfig => {
// Filter some useless plug-ins
webpackConfig.plugins = webpackConfig.plugins.filter(p= > {
switch (p.constructor.name) {
case 'UglifyJsPlugin':
case 'CaseSensitivePathsPlugin':
case 'IgnorePlugin':
case 'ProgressPlugin':
case 'HardSourceWebpackPlugin':
return false;
}
return true;
});
// Development environment plug-in
if (isDev) {
// Package the resource analysis plug-in
webpackConfig.plugins.push(
new BundleAnalyzerPlugin()
);
}
// Production environment
if(! isDev) {// code compression beautification plug-in
webpackConfig.plugins.push(
new ParallelUglifyPlugin({
// The argument passed to UglifyJS
uglifyJS: {
output: {
// Most compact output
beautify: false.// Delete all comments
comments: false,},compress: {
// Delete all 'console' statements, compatible with Internet Explorer
drop_console: true.// Inline variables that are defined but used only once
collapse_vars: true.// Extract static values that occur multiple times but are not defined as variables to reference
reduce_vars: true.// Not in use
unused: false}},cacheDir: './cache'.workerCount: os.cpus().length
}),
);
// Ignore the moment language package
webpackConfig.plugins.push(new webpack.IgnorePlugin(/^\.\/locale$/./moment$/));
}
// Progress bar display
webpackConfig.plugins.push(
new WebpackBar({
profile: true.// reporters: ['profile']}));// Separate the packages under node_modules into vendor.hash8.js
const nodeModulesPath = path.join(__dirname, './node_modules');
webpackConfig.plugins.push(new CommonsChunkPlugin({
name: 'vendor'.filename: '[name].[hash:8].js'.minChunks: function (module, _) {
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(nodeModulesPath) >= 0)}}));// Import external CDN resources
webpackConfig.plugins.push(
new HtmlWebpackExternalsPlugin({
externals: [{module: 'react'.entry: isDev ?
'https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.development.js' :
'https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js'.global: 'window.React'
},
{
module: 'react-dom'.entry: 'https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js'.global: 'window.ReactDOM'
},
{
module: 'bizcharts'.entry: 'http://gw.alipayobjects.com/os/lib/bizcharts/3.4.5/umd/BizCharts.min.js'.global: 'window.BizCharts'
},
{
module: '@antv/data-set'.entry: 'https://cdn.jsdelivr.net/npm/@antv/[email protected]/build/data-set.min.js'.global: 'window.DataSet'
},
{
module: 'echarts'.entry: 'https://cdnjs.cloudflare.com/ajax/libs/echarts/4.8.0/echarts.min.js'.global: 'window.echarts'
},
{
module: 'jquery'.entry: 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js'.global: 'window.jQuery'
},
{
module: 'xlsx'.entry: 'https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.0/xlsx.full.min.js'.global: 'window.XLSX'
},
{
module: 'lodash'.entry: 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js'}].files: ['index.html']}));// Add threadloader to rules (for now only js)
// Filter unneeded Loaders
// webpackConfig.module.rules = webpackConfig.module.rules.filter(r =>
// ['/\\.(sass|scss)$/', '/\\.html$/'].indexOf(r.test + '') < 0);
webpackConfig.module.rules.forEach(r= > {
switch (r.test + ' ') {
case '/\\.less$/':
case '/\\.css$/':
// r.use = ['happypack/loader?id=styles'];
// r.exclude = /node_modules/;
break;
case '/\\.js$/':
case '/\\.jsx$/':
if (r.use && Array.from(r.use).indexOf('thread-loader') < 0) {
r.use.splice(0.0.'thread-loader');
r.exclude = /node_modules/;
}
break;
default:
break; }});return smp.wrap(webpackConfig);
};
Copy the code