Webpack performance optimization

  1. How do I analyze performance data
  2. Optimization of compile time
  3. Optimization of compilation volume
  4. How to run faster

This article starts with a summary of the interaction between the electron main thread and the render thread from the previous article

How do I analyze performance data

It mainly introduces the use of some common tools

Install friendly-errors-webpack-plugin, node-notifier,speed-measure- webPack5-plugin, and webpack-bundle-Analyzer

$ yarn add friendly-errors-webpack-plugin speed-measure-webpack5-plugin webpack-bundle-analyzer -D
$ yarn add node-notifier
Copy the code

Friendly-errors-webpack-plugin: Identifies certain categories of Webpack errors and provides a better development experience

Node-notifier: supports multiple platforms. Since electron can also call Node-notifier, it is not installed as a development dependency

Speed-measure -webpack5-plugin: the speed-measure-webpack5-plugin is used in Webpackage 4 to analyze the packaging speed

Webpack-bundle-analyzer: Monitors the volume of packages. It needs to be used with webpack-CLI

Modify the WebPack configuration

render-process/config/webpack.common.js

const webpack = require('webpack') const path = require('path'); const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin') const notifier = require('node-notifier') Const icon = path.join(__dirname,' error.png ') module.exports = {entry: {// defaults to main main: './render-process/main/indexx' }, / / directory context, the context root directory https://webpack.docschina.org/configuration/entry-context/#context: process. The CWD (), the output: { filename: "[name].[hash].js", path: path.join(__dirname, ".. /dist-main"),}, target: "electron-renderer", resolve: { [" ts "and" benchmark ", "js", "JSX", "json"), alias: {/ / "@" : path. Resolve (" SRC "), / / configuration @ can point to the SRC directory},}, the module: { rules: [{ test: /\\.tsx?\$/, loader: "ts-loader", exclude: /node_modules/, }], }, plugins: [ // https://www.npmjs.com/package/friendly-errors-webpack-plugin new FriendlyErrorsWebpackPlugin({ onErrors:(severity, errors)=>{ console.log(errors,'errorserrorserrorserrorserrors') const error = errors[0] notifier.notify({ Title: 'webpack compilation fails, the message: severity + ":" + error. The name, the subtitle: error. File | |', icon,})}})],}Copy the code

/render-process/main/indexx’, NPM run build:react

Packaging time analysis, introduce SpeedMeasureWebpack5Plugin

render-process/config/webpack.common.js

const SpeedMeasureWebpack5Plugin = require('speed-measure-webpack5-plugin') const smp = new SpeedMeasureWebpack5Plugin() Module. Exports =smp.wrap({entry: {// default is main main: './render-process/main/index'}, //... })Copy the code

Let’s look at the information provided by SMP

DONE Compiled successfully in 4800ms 12:43:38pm SMP ⏱ >> General output time took 4.81 secs SMP ⏱ Each plugin available Plugins > > FriendlyErrorsWebpackPlugin took 0.011 secs SMP ⏱ Loaders > > loader when ts - loader took 2.077 secs Module count = 1 >> Time modules with no loaders took 0.23 secs module count = 9 >> Special extra work Html-webpack-plugin took 0.012 secs module count = 1Copy the code
Package file module size analysis, introducing Webpack-bundle-Analyzer

render-process/config/webpack.common.js

const SpeedMeasureWebpack5Plugin = require('speed-measure-webpack5-plugin') const smp = new SpeedMeasureWebpack5Plugin()  const {BundleAnalyzerPlugin} =require('webpack-bundle-analyzer') module.exports =smp.wrap({ // ... plugins: [ new FriendlyErrorsWebpackPlugin({ // ... }), // https://www.npmjs.com/package/webpack-bundle-analyzer new BundleAnalyzerPlugin(), ], })Copy the code

/package.json adds an NPM command,–progress to indicate the monitoring process

"build:reactWithProgress": "webpack --progress --config render-process/config/webpack.prod.js"

Executing this command opens http://127.0.0.1:8888/ in the default browser

Save the packaged report locally
New BundleAnalyzerPlugin({analyzerMode:'disable', // do not start the web server to display the packaged report generateStatsFile:true // generate the report file})Copy the code

Json file (render-process/dist-main/stats.json)

Use the Web to view the local JSON package report file

Package. json adds the command to specify the address of the JSON file

"webpackAnalyzer": "webpack-bundle-analyzer --port 8888 ./render-process/dist-main/stats.json"
Copy the code

Optimization of compile time

Webpack.docschina.org/guides/buil…

There are two main ideas:

  • Reduce the number of files to be processed
  • Narrow down what you’re looking for

1. resolve

You can specify extensions instead of requiring or importing a file extension

Resolve: {/ / default suffix, introduced one by one to find, sorting strategy: file more put in front of, for example. Ts extensions: [", "benchmark" ts ", "js", "JSX", "json"],},Copy the code

2. alias

Configuring aliases speeds up WebPack’s search for modules

The bootstrap, for example, suppose that we should introduce the bootstrap. CSS, normally to import (‘ the bootstrap/dist/CSS/bootstrap CSS ‘), Since the main of the bootstrap package is dist/js/bootstrap.js, import(‘bootstrap’) cannot be used.

The bootstrap package. The json

  "style": "dist/css/bootstrap.css",
  "sass": "scss/bootstrap.scss",
  "main": "dist/js/bootstrap.js",
Copy the code

Configure aliases to solve this problem and speed up the module’s search

resolve: {
        alias: {
            bootstrap:path.resolve(__dirname,'node_modules/bootstrap/dist/css/bootstrap.css')
        }
    },
Copy the code

3. MainFields and mainFiles

When the module finds the file, it refers to the main field of package.json of the corresponding package. If we don’t want to use main, we can use mainFields

Resolve: {// target==web or target==webworker,mainFields default: ['browser','module','main'] ['module','main'] mainFields:['style], // If package.json does not have those fields, use index file mainFiles:[index]},Copy the code

4. modules

When node_modules can’t be found in the current directory, Webpack looks for the parent directory, and specifying the modules path directly speeds up the search

resolve: {
        modules:[
            'c:/node_modules'
        ]
    },  
Copy the code

5.oneOf

OneO means that only one loader in the array can be matched. Once one loader is found, no other loader will be matched

    module: {
        rules: [{
            oneOf:[
                {
                    test: /\\.tsx?\$/,
                    loader: "ts-loader",
                    exclude: /node_modules/,
                }
            ]
        }],
    }
Copy the code

6. externals

If we want to reference a library and we don’t want webpack to be packaged without affecting our use in development, we can use externals

The React example

 externals: {
    'react': 'React',
    'react-dom': 'ReactDOM'
  }
Copy the code

JQuery example

Jquery :' jquery'} // Assign window. jquery to \$import \$from 'jquery'Copy the code

HTML to add the CDN

CDN_LIST field is configured in HtmlWebpackPlugin, and the use of different CDN addresses in development environment and production environment or ejS template engine syntax in index.html is controlled by functionCopy the code

7. resolveLoader

ResolveLoader: configure the loader’s search directory, default node_modules, can extend the search scope (local loader function)

resolveLoader:{
    modules:[path.resolve(__dirname,'xxx'),'node_modules']
}
Copy the code

8. noParse

The module.noParse field allows you to configure which module files do not need to be parsed, improving the overall build speed

Example: Files matching title.js will not be parsed when configured, and syntax such as import require cannot be used in title.js

   module: {
        noParse:/title.js/,
        rules: [{
            oneOf:[
                {
                    test: /\\.tsx?\$/,
                    loader: "ts-loader",
                    exclude: /node_modules/,
                }
            ]
        }],
    }
Copy the code

9.IgnorePlugin

Example: Use IgnorePlugin to ignore localized content

Install the moment YARN add moment

Refer to my moment

import moment from 'moment'
console.log(moment)
Copy the code

Comment out the configuration items and open the Package Analysis Web page

New BundleAnalyzerPlugin({// analyzerMode:'disable', // do not start the web server to display the packaged report // generateStatsFile:true, // Generate the report file}),Copy the code

When we look at the packaging report, we find that the local of moment occupies most of the volume of moment

Use IgnorePlugin for optimization

    plugins: [
        // https://www.npmjs.com/package/friendly-errors-webpack-plugin
        new FriendlyErrorsWebpackPlugin({
            // ...
        }),
        // https://www.npmjs.com/package/webpack-bundle-analyzer
        new BundleAnalyzerPlugin({
            // ...
        }),
        // https://webpack.docschina.org/plugins/ignore-plugin/
        new webpack.IgnorePlugin({
            resourceRegExp: /^\\.\\/locale\$/,
            contextRegExp: /moment\$/,
        })
    ],
Copy the code

It’s much smaller when you remove the language pack

Manually reference the language pack

index.tsx

import moment from 'moment'
import 'moment/locale/zh-cn'
console.log(moment)
Copy the code

10. thread-loader

It is not recommended unless the resources are very large and large

Install yarn add thread-loader -d

My side of the configuration on the packaging error, this will not elaborate

11. Leverage caching

Babel-loader uses the cache. You can try to use the cache when repacking to improve the packaging speed. The default location is node_modules/

use:[
    {
        loader:'babel-loader',
        options:{
            cacheDirectory:true
        }
    }
]
Copy the code

  cache-loader

The installation

yarn add cache-loader -D

Configure cache-loader. In order to see the effect, I will import bootstrap and Lodash

Module: {rules: [{oneOf: [{test: /\\.tsx?\$/, // exclude node_modules. Exclude takes precedence over include. /node_modules/, include: path.resolve(__dirname, '../'), use: [ "cache-loader", "ts-loader" ], }, { test: /\\.css\$/, use:[ 'cache-loader', 'style-loader', 'css-loader' ] } ] } ], },Copy the code

When there is no cache, it takes 10 seconds

I used 8 seconds when I had a cache

By comparison of time, the packaging time changed from 10S to 8S. It can be seen that the change is not great. Maybe my project is too small

hard-source-webpack-plugin

Webpack5 has module caching built in, so you don’t need to use this plugin issues6527

Optimization of compilation volume

Optimize – CSS -assets-webpack-plugin: optimize and compress CSS

Terser-webpack-plugin: Optimize and compress JS

Image-webpack-loader: Image compression and optimization

The installation

yarn add optimize-css-assets-webpack-plugin terser-webpack-plugin file-loader image-webpack-loader -D

Js compressed

/ / optimizations https://webpack.docschina.org/configuration/optimization/#root optimization: {/ / open, the default is true to minimize: true, minimizer: [new TerserPlugin({ exclude:/nodu_modules/ })], },Copy the code

Turn off Optimization,main package volume

Turn on Optimization,main package volume

CSS compression

   plugins:[
        new OptimizeCssAssetsWebpackPlugin()
    ]
Copy the code
  • Clearing useless CSS

Purgecss-webpack-plugin: Clears unnecessary CSS

Mini-css-extract-plugin: used with purgecss-webpack-plugin, extract CSS glob separately: used to find files

The MiniCssExtractPlugin may conflict with smp.wrap, so I will remove the SMP

const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const PurgeCssWebpackPlugin =require('purgecss-webpack-plugin') const glob = require('glob') ... module: { rules: [ { oneOf: [ { // ... }, { test: /\\.css\$/, use: / / / 'cache - loader, / / MiniCssExtractPlugin. Replace' style - loader loader, MiniCssExtractPlugin. Loader, 'css-loader' ] } ] } ], }, plugins: [ new FriendlyErrorsWebpackPlugin({ // ... } }), new MiniCssExtractPlugin( { filename:'[name].css', } ), New PurgeCssWebpackPlugin ({/ / to purify the all files in directory paths: glob. Sync (\ ` \ ${path. Resolve (__dirname, '.. / main ')} / * * / * \ `, {nodir: true}) }, //...]Copy the code

After optimization, the CSS is removed from the main package, and the main package is suddenly smaller

Also, the CSS we packaged was only 3KB, or closer to 200KB if we packaged it in full

HTML compression

The htML-webpack-plugin automatically enables minify when production is true if mode is ‘production’, otherwise false

Image compression (official website copy configuration option)

{ test: /\\.(gif|png|jpe? g|svg)\$/i, use:[ 'file-loader', { loader: 'image-webpack-loader', options: { mozjpeg: { progressive: true, }, // optipng.enabled: false will disable optipng optipng: { enabled: false, }, pngquant: { quality: [0.65, 0.90], speed: 4}, GIFsicle: {interlaced: false,}, // The webp option will enable webp webp: {lace: 75}}},]}Copy the code

Tree Shaking, only using methods into bundles (taking advantage of es6 module features)

Production mode is enabled by default

The code segment

  • Entry point segmentation

  • If there are duplicate modules (Lodash) between the chunks, those duplicate modules will be introduced into each bundle

  • Not flexible enough and unable to dynamically split code from core application logic

    entry: { index: “./src/index.js”, login: “./src/login.js” }

Dynamic import and lazy load

  • Split code needs an on-demand loading opportunity

  • For the first time the page needs to be loaded directly, as soon as possible to show the user, some of the function points that rely on a lot of code can be loaded on demand

  • The functions of the website are divided into chunks for each category. Enabling splitChunks will create separate files

  • You can also write magic notes

    document.querySelector(‘#clickBtn’).addEventListener(‘click’,() => { import(‘./hello’).then(result => { console.log(result.default); }); });

According to the need to load

The React project implements on-demand loading: react.lazy

use

const MyLazy = React.lazy(()=>import('./MyLazy'))
Copy the code

MyLazy must use Suspense wrapping and fallback is the loading component

<Suspense fallback={<div>loading</div>}>
                <MyLazy />
            </Suspense>
Copy the code

Preload

  • Preload is usually used for key resources used in the page, including key JS, fonts, and CSS files
  • Preload will increase the load order weight of resources, so that key data can be downloaded in advance, optimize the speed of page opening
  • By adding a preloaded comment to a resource, you indicate that the module needs to be used immediately

Asynchronous/delayed/inserted scripts (regardless of location) are Low in network priority

<link rel="preload" as="script" href="utils.js">

import(
  \`./utils.js\`
  /* webpackPreload: true */
  /* webpackChunkName: "utils" */
)
Copy the code
  • prefetch

    • Different from preload, prefetch tells the browser about a resource that is likely to be used in the future, and the browser will load the corresponding resource when it is free. If the user’s behavior can be predicted, such as lazy loading or clicking on another page, it is equivalent to preloading the required resource in advance

<link rel="prefetch" href="utils.js" as="script">

button.addEventListener('click', () => { import( \`./utils.js\` /* webpackPrefetch: true */ /* webpackChunkName: "utils" */ ).then(result => { result.default.log('hello'); })});Copy the code

Preload is used for resources that are necessary for the current page and Prefetch is used for resources that may be used in future pages

splitChunks

Starting with WebPack V4, CommonsChunkPlugin has been removed and replaced with Optimization.splitchunks.

configuration

Optimization: {// Enable minimization, the default is true minimize: true, minimizer: [new TerserPlugin({ exclude:/nodu_modules/ })], // splitChunks:{ chunks: The default value is all/initial/async minSize: 0, and the default value is 30KB, minChunks of the smallest size: MaxAsyncRequests: // How many modules are shared, and how many times modules are referenced before splitting // limit the number of parallel requests per import(maxInitialRequests: 5) // limit the number of parallel requests per import(maxInitialRequests: 5) By default, the name of chunk is separated by a separator (~ by default), such as vendor~}},Copy the code

Configure the splitChunks package file as shown in the figure above.

unfinished

  • Javascript compatibility
    • Because we usets-loaderCompile TSX directly without using Babel, so no consideration