preface

With the iteration of the project, front-end dependencies, JS, CSS, static resources and other requirements continue to increase, resulting in more and more redundant codes, resulting in large packaged files and slower and slower packaging speed. The packaging time of the recent project has been nearly 5 minutes, plus the dependent installation and file uploading during Jenkins deployment, a construction takes 7-8 minutes. Such a speed seriously affects the efficiency of development and release.

Therefore, aiming at this problem, a project construction and package optimization were carried out, hoping to improve the team’s work efficiency.

At the beginning, I found webpack3 was still used in the project. I didn’t want to change too much to minimize the scope of influence on the project. I made some optimization on the basis of WebPack3, but after many comparisons, the effect was always not ideal, so I simply upgraded to WebPack4. There were definitely a lot of changes to match dependencies, Webpack configuration, etc., and there was a lot of effort involved.

Upgrading to webpack4 is not explained here, but can be done by your own project. The following are some optimizations based on webpack4 configuration:

Build Help tools

Before tuning, we can take a look at the two WebPack tool plug-ins to help us see the optimization process and results more easily.

1. Construction speed detection

Speed – measure – webpack – plugin [www.npmjs.com/package/spe]…

This plugin can help us detect build speed during the WebPack packaging process, such as seeing the execution times of plugins, loaders, and so on, to focus more clearly on the areas that need to be optimized.

The configuration process is very simple, just a WebPack plug-in, install the dependency, reference and configure:

Install dependencies

# npm
$ npm install --save-dev speed-measure-webpack-plugin

# or yarn
$ yarn add -D speed-measure-webpack-plugin
Copy the code

Configure the plug-in build.js

// Reference the plug-in
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');

const smp = new SpeedMeasurePlugin();

// The configuration plug-in wraps the entire configuration object
const webpackConfig = smp.wrap({
  plugins: []});Copy the code

OK, execute the build command to see the effect!

2. Packaging visualization

Webpack – bundle – analyzer [www.npmjs.com/package/web]…

Package visual analysis, this plug-in can visually see the packaged files, file size, module inclusion relationships, dependencies, etc., for these, we can split and merge files, very convenient.

The configuration method is as follows:

Install dependencies

# npm
$ npm install --save-dev webpack-bundle-analyzer

# or yarn
$ yarn add -D webpack-bundle-analyzer
Copy the code

Reference and configure the plug-in build.js

// Reference the plug-in
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')

// Configure the plug-in
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerPort: 3030 / / the port number}})]Copy the code

Run the build command and the browser will automatically open http://127.0.0.1:3030/. You can see the image above, where the port number is the one you just configured in the plugin.


OK, now that we have the tools, let’s configure webPack optimization!

Optimize the loader

Loader, webpack can only package JS files, CSS, images, fonts and other formats of the file, is not recognized. Loader can compile, compress, package, and finally convert these modules that webPack cannot identify to JS.

In our front-end project, most of these non-JS modules are scattered in different file directories. It is not necessary to detect and process all non-JS modules globally during the packaging process, such as node_modules. In addition, some files are too deeply nested, and Loader needs to search level by level, which will definitely affect efficiency.

First of all, we need to simplify the file directory level in the project. We recommend no more than 3 levels.

At the same time, the loader search scope is optimized to ignore directories that do not need to be compiled.

module.exports = {
  ...
  module: {
    rules: [{test: /\.(js|jsx)$/,
        exclude: [resolve('.. /node_modules')].// Mask unwanted files (folders) (optional)
        use: ['babel-loader'] {},test: /\.css$/.// exclude: [resolve('../node_modules')], // css-loader needs to check whether the project directly references the dependency CSS file. If so, the dependency directory cannot be excluded
        use: ['style-loader'.'css-loader'] {},test: /\.less$/,
        exclude: [resolve('.. /node_modules')].// Mask unwanted files (folders) (optional)
        use: ['style-loader'.'css-loader'.'less-loader'] {},test: /\.(png|jpg|jpeg|gif)(\? . +)? $/,
        exclude: [resolve('.. /node_modules')].// Mask unwanted files (folders) (optional)
        use: ['url-loader'] {},test: /\.(woff2? |eot|ttf|otf)(\? . *)? $/,
        exclude: [resolve('.. /node_modules')].// Mask unwanted files (folders) (optional)
        use: ['url-loader']},]}}Copy the code

Note: csS-loader: You need to check whether there are CSS files in the project that directly reference node_modules. If there are, you cannot exclude the dependent directory, otherwise the package will report an error.

Compress images and fonts

In the loader configuration above, we use urL-loader to compile images, fonts and other files, but it should be noted that not all images and fonts need to use Loader.

Because urL-loader packages images and fonts into Base64, the base64 of some large images may be larger than the original memory, so you need to limit the size of resources that can be packaged. For resources that exceed the limit, you can directly import them as separate files in a path.

module.exports = {
  ...
  module: {
    rules: [{...test: /\.(png|jpg|jpeg|gif)(\? . +)? $/,
        exclude: [resolve('.. /node_modules')].// Mask unwanted files (folders) (optional)
        loader: 'url-loader'.options: {
          esModule: false.// Set this to false, otherwise SRC to "[object Module]"
          limit: 10000.// url-loader contains file-loader. Instead of file-loader, images smaller than 10000B are imported in Base64 mode, and images larger than 10000B are imported in path mode
          name: isDev ? 'image/[name][hash:8].[ext]' : 'image/[name].[contenthash:8].[ext]'}}, {test: /\.(woff2? |eot|ttf|otf)(\? . *)? $/,
        exclude: [resolve('.. /node_modules')].// Mask unwanted files (folders) (optional)
        loader: 'url-loader'.options: {
          limit: 10000.// The font smaller than 10000B is imported as base64, and the font larger than 10000B is imported as path
          name: isDev ? 'font/[name][hash:8].[ext]' : 'font/[name].[contenthash:8].[ext]'}},]}}Copy the code

Note here:

  • Image resourceesModuleNeed to set tofalse, otherwise quotedsrc[object Module], cannot display
  • You are advised to store these types of files in different directories for easy management
  • Use the file after packagingcontenthashNamed so that files that do not change often can be cached, improving the speed of secondary packaging

The separation of CSS

In the default configuration, the CSS is embedded within packaged in js file, this will certainly lead to js file is very big, so we need to separate the CSS code, as a separate directories and files, so the CSS and js can parallel packaging, one advantage is that can be downloaded in parallel at the same time, to load faster and scripts.

Here we use the mini-CSs-extract-plugin (extract-text-webpack-plugin is available in Webpack 3, but not in WebPack 4) :

1. Install dependencies

# npm
$ npm install --save-dev mini-css-extract-plugin

# or yarn
$ yarn add -D mini-css-extract-plugin
Copy the code

2. Configure the plug-in

config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: isDev ? 'css/[name][hash:8].css' : 'css/[name].[chunkhash:8].css'.chunkFilename: isDev ? 'css/[id][hash:8].css' : 'css/[id].[chunkhash:8].css'})],module: {
    rules: [{...test: /\.css$/./ / local development environment, style - loader and MiniCssExtractPlugin. Loader
        use: [isDev ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader'] {},test: /\.less$/./ / local development environment, style - loader and MiniCssExtractPlugin. Loader
        use: [isDev ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader'.'less-loader']},],},};Copy the code

Note:

  • In the local development environment,style-loaderMiniCssExtractPlugin.loaderConflicts exist and need to be set separately
  • You are advised to store them in a separate CSS directory
  • Use the file after packagingchunkhashCache files that do not change frequently to improve the speed of secondary packaging

Compress CSS

Webpack4 better is you can configure optimization. Minimize [webpack.js.org/configurati… will default to compress CSS, js file, at the same time can also support plug-in integration.

Optimize – CSS-assets-webpack-plugin for CSS compression

1. Install dependencies

# npm
$ npm install --save-dev optimize-css-assets-webpack-plugin

# or yarn
$ yarn add -D optimize-css-assets-webpack-plugin
Copy the code

2. Configure the plug-in

build.js

const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin') / / CSS compression

module.exports = {
  ...
  optimization: {
    minimizer: [
      / / compress CSS
      new OptimizeCSSAssetsPlugin({
        cssProcessorOptions: {
          // Remove comments
          discardComments: {
            removeAll: true}}}),]}}Copy the code

Note:

  • It only needs to be compressed at package time, so configure it separately in build.js

The separation of js

During the webPack default packaging process, we can see that all js are packed into one file, which causes the main file to be very large. Third party dependencies, like those that don’t update very often, generally don’t change, and if packaging is performed every time, it’s definitely not what we want.

In webpack3, most of our schemes are implemented by CommonsChunkPlugin + cache strategy; And webpack4 brings us a better use of tools: splitChunks [webpack.js.org/plugins/spl…

SplitChunks is an integrated feature by default and can be configured without installing plug-ins.

For example, we could package the infrequently updated React correlation into a baseChunks; Antd, ICONS, echarts, emojis and so on are packed into one uiChunks; The rest of the business js etc. are packaged into the default chunk:

build.js

module.exports = {
  optimization: {...splitChunks: {
      chunks: 'all'./ / initial (initialization) | async (dynamic loading) | all (all)
      minSize: 30000.// // > 30K will be unpacked by Webpack, default 0
      minChunks: 1.// Split if the number of references is greater than or equal to this number. The default value is 1
      maxAsyncRequests: 5.// Maximum number of asynchronous requests. Default: 1
      maxInitialRequests: 5.// Maximum number of initial requests, default 1
      name: true.automaticNameDelimiter: '. '.// Pack the delimiter
      cacheGroups: {
        / / library
        baseChunks: {
          name: 'base.chunks'.// Name of the chunk to be separated
          test: (module) = > (/react|react-dom|react-router-dom|react-redux|redux|axios|moment|lodash/.test(module.context)),
          priority: 20 // Package priority
        },
        // UI, icon, chart, emoticon, etc
        uiChunks: {
          name: 'ui.chunks'.// Name of the chunk to be separated
          test: (module) = > (/antd|@ant-design\/icons|echarts|emoji-mart/.test(module.context)),
          priority: 10 // Package priority
        },
        // Package the rest of the public code
        default: {
          name: 'common.chunks'.// Name of the chunk to be separated
          minChunks: 2.// Introduce twice or more to be packaged
          priority: 5.reuseExistingChunk: true // You can set whether to reuse existing chunks and not create new chunks}}},}}Copy the code

Note:

  • The chunkhash name of the js file must be configured, otherwise the hash will change every time the package is packaged, or the file that is not expected to change will be updated:
module.exports = {
   // Export configuration
  output: {
    path: resolve('.. /dist'),
    filename: isDev ? 'js/[name].[hash:8].js' : 'js/[name].[chunkhash:8].js'.publicPath: isDev ? '/' : '/'}},Copy the code

The result after packaging is shown below:

Compression js

Uglifyjs-webpack-plugin can be used to compress js files and reduce the size of js files.

1. Install dependencies

# npm
$ npm install --save-dev uglifyjs-webpack-plugin

# or yarn
$ yarn add -D uglifyjs-webpack-plugin
Copy the code

2. Configure the plug-in

build.js

const UglifyJsPlugin = require('uglifyjs-webpack-plugin') / / js compressed

module.exports = {
  ...
  optimization: {
    // A custom js optimization configuration will override the default configuration
    new UglifyJsPlugin({
      parallel: true.// Use multiple processes running in parallel to speed up builds
      sourceMap: true.// It must be opened to generate map files
      cache: true.uglifyOptions: {
        output: {
          comments: false.// Remove the comment
          ascii_only: true
        },
        compress: {
          drop_console: false.drop_debugger: true.comparisons: false}})}}Copy the code

Note:

  • It only needs to be compressed at package time, so configure it separately in build.js
  • Custom JS optimization configuration will override the default configuration

Multiple processes

Since WebPack is single-threaded based on Node.js, when we compile and package files, we have to process tasks one by one, which is bound to be slow to build.

Happypack is designed to solve this problem by enabling multiple processes to split tasks into multiple sub-processes and execute them concurrently. When the sub-processes finish processing tasks, they send the results to the main process to speed up packaging.

Strictly speaking, should be multi-process: because JS is a single-thread model, to play the multi-core CPU ability, can only be achieved through multi-process, and can not be achieved through multi-thread.

For example, configure two loaders with multiple processes using HappyPack and reference the configured Loaders in Rules

1. Install dependencies

# npm
$ npm install --save-dev happypack

# or yarn
$ yarn add -D happypack
Copy the code

2. Configure the plug-in

const os = require('os')
const Happypack = require('happypack') // Multi-process packaging

// Enable the process pool to use the maximum number of CPU cores
const happyThreadPool = Happypack.ThreadPool({
  size: os.cpus().length
})

module.exports = {
  plugins: [...// Multiple different processes can be configured
    / / js process
    new Happypack({
      id: 'js'./ / process name
      threadPool: happyThreadPool,
      // The configuration mode is the same as that of loader
      use: [
        'babel-loader']}),/ / less process
    new Happypack({
      id: 'less'./ / process name
      threadPool: happyThreadPool,
      // The configuration mode is the same as that of loader
      use: [{loader: 'css-loader'.options: {
            importLoaders: 1.modules: true // less modules support, be sure to enable}}, {loader: 'less-loader'}]}]}Copy the code

3. Application process

After happypack is configured, it can be used in loader configuration:

module.exports = {
  module: {
    rules: [{test: /\.(js|jsx)$/,
        exclude: [resolve('.. /node_modules')].// Mask unwanted files (folders) (optional)
        use: 'happypack/loader? id=js'},... {test: /\.less$/,
        exclude: [resolve('.. /node_modules')]./ / happypack MiniCssExtractPlugin loader will have error, so set up separately
        use: [isDev ? 'style-loader' : MiniCssExtractPlugin.loader, 'happypack/loader? id=less']},]}}Copy the code

Note:

  • loadertheidBe sure to configure withhappypack, so that the corresponding process can be loaded

As shown in the following figure, you can see the number of running processes:


OK, at this point, the optimized configuration is basically complete!

Take a look at the final result. The optimized packaging was completed in less than 1 minute, compared to the initial 5-6 minutes! The file directory is also very clear after packaging!

Tips: You are advised to run the build command after configuring an item, which helps you find and troubleshoot problems in time.


Finally, note the problems you may encounter during the configuration process:

Possible problems

1. Failed to load resource: net::ERR_FILE_NOT_FOUND

Cause: The output path of the package file is incorrect

Solution: Modify the build.js export configuration

output: {
    publicPath: '/'
}
Copy the code

2. After packaging, the image address becomes [Object Module].

Cause: The problem is that the esModule of file-loader defaults to True in new versions

Solution: Manually change it to false

{
  test: /\.(png|jpe? g)(\? . *)? $/,
  loader: 'url-loader'.options: {
    esModule: false.limit: 10000}}Copy the code

3. After packaging, index. HTML is imported without quotes

Cause: The problem is that quotes are removed during compression

Solution: Find Minify in build.js, set removeAttributeQuotes: false, or remove the entire Minify

13, Warning: [Mini-CSS-extract-plugin] Conflicting order between Entrypoint Mini-CSs-extract-plugin =

Cause: A warning is generated if you reference the same two CSS files in different order in different files

Solution:

  • Change the order of CSS files in the faulty JS
  • If it cannot be modified, consider turning off warnings. configurationstatsRemove warnings, stats document:Webpack.js.org/configurati…
module.exports = {
  ...
  // Log setup at build time
  stats: {
    children: false}},Copy the code

5, WARNING: WARNING in asset size limit: The following asset(s) exceed The recommended size limit (244 KiB)

Cause: The size of the packaged file exceeds the default limit

Solution: Modify the source file if you can; If this is not possible, consider turning off warnings or modifying restrictions. Build.js add configuration:

module.exports = {
  // How does the configuration display performance prompts
  performance: {
    Hints: false, // Disable or configure the following warning limits directly
    hints: 'warning'.maxEntrypointSize: 50000000.// The maximum size of the entry point, 250KB by default
    maxAssetSize: 30000000 // Controls when WebPack generates performance prompts based on individual resource volumes. Default is 250KB}}Copy the code