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:

  • usingBundleAnalyzerPluginAnalyze dependent packages
  • opengzipCompression, this requires the server to do configuration can be reduced50%Left and right package volume
  • Separate the partially referenced external dependent libraries toCDNStatic resource import mode
  • Package filteringmomentThe language pack
  • Separate dependency packages (can be the wholenode_modules) tovendor.jsfile

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-consumingwebpackThe plug-in
  • Upgrade the code compression plug-in and enable caching
  • Different configurations for development and productionwebpackThe plug-in
  • usingSpeedMeasurePluginPlug-ins analyze time-consuming packaging

Step 2, create a customwebpackconfiguration

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-builtwebpackThe 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
  • addedBundleAnalyzerPluginAfter the plug-in is packaged, it will automatically open immediately127.0.0.0:8888Web 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.jsonThe unified declaration of solution). (TIPS:roadhogThe plug-in itself is provided and only needs to be configured in the package commandANALYZE=trueEnvironment variables, such as:cross-env ANALYZE=true roadhog buildIn 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.

  • addedSpeedMeasurePluginAfter 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. OpengzipThe 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 packagesCDNWay to introduce

Why use CDN to introduce dependency packages? I think there are several reasons:

  • (1)CDNBy using common resources and services, you can reduce the size of the package and reduce access and traffic to your own servers
  • (2)CDNYou can do a public cache, based on the network of visitors to read data, access faster
  • (3)CDNIt will not be affected by project release and has better optimization in browser caching for visitors
  • (4)CDNSimilar 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 dynamicallyCDNDomain names break the parallel limits of browsers

So, how do we configure it? Here we have two ways:

  • (1) Configuration.webpackrc.jstheexternalsAnd the manual inindex.ejsIn the configuration<script... />
  • (2) Use plug-insHtmlWebpackExternalsPlugin

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 environmentmomentThe 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_modulesAnd 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.jsThe 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 upgradeUglifyJsPluginforParallelUglifyPlugin

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