1. Optimization of packaging results

Optimization of volume space and network requests

Compression volume optimization:

/ / js compressed
`UglifyJSPlugin`: Delete allconsoleStatement, the most compact output, with no Spaces and tabs, and all comments removed`Tree-Shaking`: Eliminate useless code and reduce the size of packaged code/ / CSS compression
`optimize-css-assets-webpack-plugin`
`PurifyCSSPlugin`: Need to cooperate` extract-text-webpack-plugin`It is used to remove unused CSS code, similar to Tree Shaking in JS.Copy the code

Accelerated network request:

/ / the CDN acceleration
1.Index.html is put on its own server, static resources js and CSS are put on the CDN2.The file names of static resources are configured to be output as contentHash in Webpack and repackaged only if the content changes3.Different types of resources are placed on different domain names (because of HTTP1)1.Under this version of the protocol, browsers limit the number of concurrent requests to the same domain name to6A)// CommonsChunkPluginA web site is usually composed of multiple pages, each of which is an independent, single-page application, relying on the same style files, technology stacks, etc. If these common files are not extracted, each chunk packed with a single page contains the common code, which is equivalent to n duplicate code transfers. If the public file is extracted from a file, then when the user visits a web page, loads the public file, and then visits other pages that rely on the public file, the file is directly used in the browser's cache, and the public file is transferred only once// Split the code to load on demandOne of the problems of single-page applications is that a single page is used to host complex functions, and the file volume to be loaded is large. If not optimized, the first screen will take too long to load, affecting the user experience. Doing on-demand loading can solve this problem. Specific methods are as follows:1.Categorize site features into relevant categories2.Each category is merged into a Chunk and the corresponding Chunk is loaded on demandCopy the code

Tree-shaking (Built-in optimization in Prod mode)

Tree-shaking resolves to Tree Shaking, essentially eliminating useless code

Tree-shaking: The ability to wrap code at the module level so that only the modules that are referenced and executed are included, and the modules that are not referenced or not executed are removed for packet reduction

// math.js
export const add = (a, b) = > {
  console.log( a + b );
}
export const minus = (a, b) = > {
  console.log( a - b );
}

//math.js
import { add } from './math.js';
add(1.2);
Copy the code

Minus is not used here, but it is packaged into the bundle. Tree-shaking can help us remove the unused minus.

Note: tree-shaking is only for PROD (tree-shaking is enabled by default in PROD). In ProD, redundant code minus is removed. The dev environment, even if tree-shaking is configured, will still package useless code into bundles, but with comments attached.

Configuration:

// Add sideEffects to package.json:

{
  "name": "webpack"."version": "1.0.0"."description": ""."sideEffects": false.// Enable tree_shaking for all files
  // ...
}


// webpack.dev.js(not required under prod)
const devConfig = {
  // ...
  optimization: {
    usedExports: true,},// ...
}
Copy the code

Tree_shaking will assume that you have not exported any modules. In the packaging process, tree_shaking will ignore it, so there is no index.less file in the packaged file
import './index.less'
import '@babel/polly-fill'
Copy the code

At this point we need to configure sideEffects as follows:

// package.json
"sideEffects": [
  "*.less"."@babel/polly-fill",]Copy the code

We don’t tree_shaking when we encounter several modules specified in the sideEffects array above


Limitations of tree-shaking?

  1. ES6 modules can only be statically declared and referenced, not dynamically introduced and declared. (commonJS not supported)

  2. Can only handle module level, not function level redundancy;

Because Webpack’s tree-shaking is based on dependencies between modules, it is not possible to remove useless code within a module itself.

  1. Only jS-related redundant code can be processed, not CSS redundant code.

Currently Webpack only deals with dependencies on JS files, CSS redundancy does not provide a good tool. You can do this with PureCss.

CommonsChunkPlugin extracts common code

Extract the business common code that multiple pages depend on into common.js:

const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
/ /...
plugins:[
    new CommonsChunkPlugin({
        chunks: ['a'.'b'].// From which chunks
        name:'common'.// The extracted common part forms a new chunk})]Copy the code

Find the base library you depend on, write a base.js file, and extract the common code from common.js into base. Common.js removes the base code (because conmmon may also contain base code import ‘react’;). , while base.js remains unchanged.

//base.js
import 'react';
import 'react-dom';
import './base.css';
//webpack.config.json
entry:{
    base: './base.js'
},
plugins: [new CommonsChunkPlugin({
        chunks: ['base'.'common'].name:'base'.//minChunks:2, which indicates the minimum number of chunks that a file must have in order to be extracted, in case there is no code in common.js})]Copy the code

You get base library code base.js, common. Js without the base library, and the page’s own code file xx.js. The pages are referenced in the following order: base.js–> common.js–> xx.js


The following three plug-ins are all packaged with common code and are evolved versions of packaged with common code plug-ins

CommonsChunkPlugin -- > SplitChunksPlugin -- > DllPluginCopy the code

CommonsChunkPlugin problem:

Suppose our file is configured like this:

minChunks: 2

entryA: vuex vue Acomponent
entryB: vue axios BComponent
entryC: vue vuex axios CComponent
Copy the code
// The output file is:
vendor-chunk: vuex vue axios
chunkA~chunkC: only the Component
Copy the code

Problems: entryB does not use Vuex, entryA does not use Axios, but from the output files, both entryA and entryB introduce some “dirty” modules, which is not good.


SplitChunksPlugin: Based on the CommonsChunkPlugin, not all references are bundled into one chunk. For files that are partially shared but have a threshold too small, separate output files are not created because the size is not worth making a new request. (Cache policy configured in cacheGroup)

DLLPlugin: In addition to webpack.config.js, a new webpack.dll.config.js file will be created in the project to configure DLL packaging.

Webpack.dll.config. js packs all third-party library dependencies into a bundle DLL and generates a file named manifest.json.

The manifest. Json function is to map the DllReferencePlugin to related dependencies.


SplitChunksPlugin is also a separate public module, but every time it is packaged, it still handles some third-party libraries, but it can separate the third-party library files from our code and generate a separate JS file. But it still doesn’t speed up packing.

DLLPlugin builds common packages ahead of time so that they are filtered out during build time, resulting in a faster build time. So it’s relatively faster to pack

Divide different chunks and load them as needed

Juejin. Cn/post / 695585…

2. Optimization of construction process

Time optimization

Start with the following aspects:

1.Narrow down your search for files2.Reduce compilation times for base modules (using DllPlugin)3.Enable multi-process Loader conversion (HappyPack)4.ParallelUglifyPlugin (ParallelUglifyPlugin)Copy the code

include/exclude

When using loader, we need to use it in as few modules as possible. We can use include and exclude to specify which modules the Loader will only apply to and which modules it will not apply to.

// webpack.common.js
const commonConfig = {
  ...
  module: {
    rules: [{test: /\.js|jsx$/, 
        exclude: /node_modules/,
        include: path.resolve(__dirname, '.. /src'), 
        use: ['babel-loader']},... ] }},// exclude: /node_modules/ : exclude files under node_modules
// include: path.resolve(__dirname, '.. / SRC ') : only for files under SRC
Copy the code

Resolve tells Webpack how to search for files

  • resolve.extensions:

If we want to import modules without suffixes, we can do this:

import List from './list/list';
Copy the code

NPM run dev will report an error saying list cannot be found

[‘.js’, ‘.jsx’]. This means that we will first look for.js files in the specified directory and then look for.jsx files in the specified directory. If not, we will return

// webpack.common.js.const commonConfig = {
  ...
  resolve: {
    extensions: ['.js'.'.jsx'],},... }...Copy the code

But here we want to do as little as possible, because if we do, we’re going to include things like CSS and JPG, which will call multiple file look-ups, which will slow down the packaging.


  • resolve.mainFiles: Set as few values as possible to reduce the search steps for entry files

MainFields defines which entry file of the third-party module to use. Since most third-party modules use the main field to describe the location of the entry file, you can set a single main value to reduce search

If we want to use the following reference directly in index.js:

import List from './list';
Copy the code
// webpack.common.js
const commonConfig = {
  ...
  resolve: {
    mainFiles: ['index'.'list']}}Copy the code

MainFiles means that when looking for from ‘./list’ directories, it looks for./list/index.js first, and if not, it looks for./list/list.js

There are third-party modules that provide bits of code for different environments. For example, two pieces of code using ES5 and ES6 are provided respectively. The locations of these two pieces of code are written in package.json file as follows:

{"jsnext:main": "es/index.js",// "main": "lib/index.js" // "ES5"}Copy the code

Webpack decides which code to use first based on the configuration of mainFields; Webpack will go through the package.json file in the order of the array, using only the first one it finds.

If you want to use the ES6 code first, you can do this

mainFields: ['browser'.'main']	// mainFields default value

/ / webpack. Config. Js configuration
resolve: {mainFields: ['jsnext:main'.'browser'.'main']}Copy the code

  • Resolve. Alias: skip

  • resolve.modules

Resolve. Modules :[path.resolve(__dirname, ‘node_modules’)] to avoid layers of lookup. Resolve. modules tells Webpack which directories to look for third-party modules. The default is [‘node_modules’], which looks for./node_modules,.. / node_modules,.. /.. / node_modules.

// webpack.common.js
const commonConfig = {
  ...
  resolve: {
    extensions: ['.js'.'.jsx'].mainFiles: ['index'.'list'].alias: {
      alias: path.resolve(__dirname, '.. /src/alias'),},modules: [path.resolve(__dirname, 'node_modules')]},... }Copy the code

Use the DllPlugin to reduce compilation times for the base module

DllPlugin is a dynamic link library plug-in whose principle is to extract the basic modules that web pages depend on and package them into A DLL file. When a module to be imported exists in a DLL, the module is no longer packaged, but obtained from the DLL. Why does this increase build speed? The reason is that most DLLS contain common third-party modules such as React and React-dom, so as long as these modules are not upgraded, they only need to be compiled once

Use HappyPack to enable multi-process Loader conversion

In the whole construction process, the most time-consuming is the Loader’s file conversion operation, and the Webpack running on Node.js is a single-threaded model, that is, it can only be processed one file at a time, not in parallel. HappyPack can split tasks into child processes and send results to the main process.

npm i -D happypack
// webpack.config.json
const path = require('path');
const HappyPack = require('happypack');

module.exports = {
    / /...
    module: {rules: [{test:/\.js$/.use: ['happypack/loader? id=babel']
                exclude:path.resolve(__dirname, 'node_modules')
            },{
                test:/\.css/,
                use:['happypack/loader? id=css']}],plugins: [new HappyPack({
                id:'babel'.loaders: ['babel-loader? cacheDirectory']}),new HappyPack({
                id:'css'.loaders: ['css-loader'})]}}Copy the code

Js file multi-process compression

Because the UglifyjsWebpackPlugin compression plug-in is a single thread to run, and TerserWebpackPlugin can run the compression function concurrently (multi-process). Therefore, the UglifyjsWebpackPlugin is replaced by TerserWebpackPlugin.

noParse

For example, the project relies on some low-level modules, jquery. In the process of webPack and analysis, we see that jquery has been imported, so we will parse jquery again to see whether there are other third-party modules introduced into jquery, but we know that jquery is the low-level library. There are no more third libraries in jquery, so you can configure WebPack to save time by not parsing jquery

import $ from jquery
Copy the code
// webpack.config.js
module.exports = {
    module: {
        // No need to parse jquery
        noParse: /jquery/}}Copy the code

externals

If we want to reference a library and don’t want webpack to be installed, which means we don’t want NPM install to be installed, but we don’t want to use it as CMD, AMD, or Window /global in our application, we can configure externals.

By default, WebPack all third-party dependencies imported by import into a single verndor.js package, which results in an oversized vendor.js output.

By configuring externals, third-party libraries configured in externals, webPack can check import dependencies so that if the imported library appears in the configuration of externals, it will not be packaged into the final vendor.js, thereby reducing the volume of packaged output. And you can get the corresponding library from the Window object.

Using lodash:

import _ from 'lodash';
Copy the code

Externals configuration:

externals: {
  "lodash": {
        commonjs: "lodash".// If our library is running in node.js, import _ from 'lodash' is equivalent to const _ = require('lodash')
        commonjs2: "lodash"./ / same as above
        amd: "lodash".// if our library is loaded with require.js etc., equivalent to define(["lodash"], factory);
        root: "_"// If our library is used in browsers, we need to provide a global variable '_', equivalent to var _ = (window._) or (_);}}Copy the code

Tapable

Webpack is essentially a flow of events, and the workflow is a series of plug-ins; At the heart of all this is Tapable, which relies on the publish and subscribe model

Synchronous hooks

1. SyncHook SyncHook

SyncHook is essentially a publish-subscribe model

// Basic usage examples:
let { SyncHook }  = require('tapable')
class Lesson{
    constructor(){
        this.hooks = {
            arch: new SyncHook(['name'])}}tap(){
    	// tap is used to register listening callbacks on hooks
        this.hooks.arch.tap('vue'.function(name){
            console.log('vue', name)
        });
        this.hooks.arch.tap('node'.function(name){
            console.log('node', name)
        })
    }
    start(){
    	The call method of the hook is used to trigger the callback function on the hook, where Hello is passed in as an argument to the callback
    	this.hooks.arch.call('hello')}}let lesson = new Lesson()
lesson.tap()

// Prints vue hello
// Prints node hello
lesson.start()	
Copy the code
// The implementation principle of SyncHook hook
class SyncHook{
    constructor(args){
    	this.tasks = [];
    }
    // Event subscription listener
    tap(eventName, taskCallback){
    	this.tasks.push(taskCallback)
    }
    // Event publishing is triggered
    call(. args){
    	this.tasks.forEach(taskCallback= >{ taskCallback(... args) }) } }let sh = new SyncHook()
sh.tap('vue'.function(arg){
    console.log('vue', arg)
})
sh.tap('node'.function(arg){
    console.log('node', arg)
})
sh.call('hello')	// Print vue hello; Print the node hello!
Copy the code

2. SyncBailHook

SyncBailHook is the insurance hook that determines whether to execute downward; If the tap callback returns a non-undefined value, it will not be executed down.

// Basic usage examples:
let { SyncBailHook }  = require('tapable')
class Lesson{
    constructor(){
        this.hooks = {
            arch: new SyncBailHook(['name'])}}tap(){
    	// the tap function argument -- > callback returns non-undefined, so the following node callbacks will not be executed
        this.hooks.arch.tap('vue'.function(name){
            console.log('vue', name)
            return 'Stop execution'
        });
        this.hooks.arch.tap('node'.function(name){
            console.log('node', name)
        })
    }
    start(){
    	this.hooks.arch.call('hello')}}let lesson = new Lesson()
lesson.tap()

// Just print vue hello; Since vue Hello's callback returns non-undefined, node Hello will not be printed;
lesson.start()	
Copy the code
// SyncBailHook implementation principle
class SyncBailHook{
    constructor(args){
    	this.tasks = [];
    }
    // Event subscription listener
    tap(eventName, taskCallback){
    	this.tasks.push(taskCallback)
    }
    // Event publishing is triggered
    call(. args){
        let index = 0; 		// The index of the first function currently to be executed
        let ret;		// ret is the return value of the callback function
        do{
            ret = this.tasks[index++](... args) }while(ret === 'undefined' && index < this.tasks.length)
    }
}


let sh = new SyncBailHook()
sh.tap('vue'.function(arg){
    console.log('vue', arg)
    return 'Stop execution'
})
sh.tap('node'.function(arg){
    console.log('node', arg)
})
// Just print vue hello; Since vue Hello's callback returns non-undefined, node will not be printed
sh.call('hello')	
Copy the code

3. SyncWaterfallHook

SyncWaterfallHook associates the tap callback function with the return value of the previous callback function as an argument to the next callback function. The waterfall flow

// Basic usage examples:
let { SyncWaterfallHook }  = require('tapable')
class Lesson{
    constructor(){
        this.hooks = {
            arch: new SyncWaterfallHook(['name'])}}tap(){
    	// 
        this.hooks.arch.tap('vue'.function(name){
            console.log('vue', name)
            return 'Vue did well.'
        });
        // The return value of the previous callback function -- > vue will be used as the data value of the following callback function
        this.hooks.arch.tap('node'.function(data){
            console.log('node', data)
        })
    }
    start(){
    	this.hooks.arch.call('hello')}}let lesson = new Lesson()
lesson.tap()

// Print: vue hello
// Print: Node vue learned well
lesson.start()	
Copy the code
// SyncWaterfallHook implementation principle
class SyncWaterfallHook{
    constructor(args){
    	this.tasks = [];
    }
    tap(eventName, taskCallback){
    	this.tasks.push(taskCallback)
    }
    call(. args){
        let [firstTask, ...otherTasks] = this.tasks
        letret = firstTask(... args) otherTasks.reduce((item, task) = > {
            return task(item)
        }, ret)
    }
}

let sh = new SyncWaterfallHook()
sh.tap('vue'.function(arg){
    console.log('vue', arg)
    return 'Vue did well.'
})
sh.tap('node'.function(arg){
    console.log('node', arg)
})
// Print: vue hello
// Print: Node vue learned well
sh.call('hello')	
Copy the code

4. Sync hook SyncLoopHook

SyncLoopHook loops through a callback that does not return undefined

// The implementation principle of SyncLoopHook synchronization hook
class SyncLoopHook{
    constructor(args){
    	this.tasks = [];
    }
    tap(eventName, taskCallback){
    	this.tasks.push(taskCallback)
    }
    call(. args){
        this.tasks.forEach(task= > {
            let ret;
        	do{ ret = task(... args) }while(ret ! ='undefined')}}}let sh = new SyncLoopHook()
let total = 0;
sh.tap('vue'.function(arg){
    console.log('vue', arg)
    return ++total == 3 ? undefined : 'Keep learning'
})
sh.tap('node'.function(arg){
    console.log('node', arg)
})
// Print three times: vue hello
// Prints: node hello
sh.call('hello')	
Copy the code

Asynchronous hooks

Asynchrony: A callback is executed after multiple parallel requests have been executed.

There are two types of asynchrony: serial and parallel; Some may need to know the results of the previous step before proceeding to the next step; Parallelism is the need to wait for all concurrent asynchronous events to execute before executing the callback method

1. AsyncParallelHook

AsyncParallelHook Waits for all asynchronous events to complete before executing the incoming callback cb()

// AsyncParallelHook basic use
let { AsyncParallelHook }  = require('tapable')
class Lesson{
    constructor(){
        this.hooks = {
            arch: new AsyncParallelHook(['name'])}}tap(){
        this.hooks.arch.tapAsync('vue'.function(name, cb){
            setTimeout(() = > {
                console.log('vue', name)
                cb()	// tell the outside world that the asynchronous setTimeout is done
            }, 1000)});this.hooks.arch.tapAsync('node'.function(name, cb){
            setTimeout(() = > {
                console.log('node', name)
                cb()	// tell the outside world that the asynchronous setTimeout is done
            }, 1000)})}start(){
    	this.hooks.arch.callAsync('hello'.function(){
            console.log('end')}}}let lesson = new Lesson()
lesson.tap()

// Print: vue hello
// Prints: node hello
// Prints: end
lesson.start()	

// Note: End is printed only once, because inside the cb function, it is checked
Copy the code
// AsyncParallelHook principle implementation of the callback function CB version
class AsyncParallelHook{
    constructor(args){
        this.tasks = []
    }
    tapAsync(eventName, taskCallback){
        this.tasks.push(taskCallback)
    }
    callAsync(. args){
        let finalCallback = args.pop()
        let index = 0;
        
        // The done call is equal to the length of the task, and the finalCallback is called. That's why end is printed once in the basic example above
        let done = () = > {
            index++;
            if (index === this.tasks.length){
                finalCallback()
            }
        }
        this.tasks.forEach(taskCallback= >{ taskCallback(... args, done) }) } }let aph = new AsyncParallelHook(['name'])

aph.tapAsync('vue'.function(name, cb){
    setTimeout(() = > {
        console.log('vue', name)
        cb()	// tell the outside world that the asynchronous setTimeout is done
    }, 1000)
})
aph.tapAsync('node'.function(name, cb){
    setTimeout(() = > {
        console.log('node', name)
        cb()	// tell the outside world that the asynchronous setTimeout is done
    }, 1000)
})

aph.callAsync('hello'.function(){
    console.log('end')})Copy the code
// AsyncParallelHook principle implementation promise version
class AsyncParallelHook{
    constructor(args){
        this.tasks = []
    }
    tapPromise(eventName, taskCallback){
        this.tasks.push(taskCallback)
    }
    callAsync(. args){
        let finalCallback = args.pop()
        let index = 0;
        
        // The done call is equal to the length of the task, and the finalCallback is called. That's why end is printed once in the basic example above
        let done = () = > {
            index++;
            if (index === this.tasks.length){
                finalCallback()
            }
        }
        this.tasks.forEach(taskCallback= >{ taskCallback(... args, done) }) } }let aph = new AsyncParallelHook(['name'])

aph.tapAsync('vue'.function(name, cb){
    setTimeout(() = > {
        console.log('vue', name)
        cb()	// tell the outside world that the asynchronous setTimeout is done
    }, 1000)
})
aph.tapAsync('node'.function(name, cb){
    setTimeout(() = > {
        console.log('node', name)
        cb()	// tell the outside world that the asynchronous setTimeout is done
    }, 1000)
})

aph.callAsync('hello'.function(){
    console.log('end')})Copy the code

reference

Juejin. Cn/post / 684490…

Webpack. Wuhaolin. Cn / 4% BC E4% % 98%…

Monocy. Site / 2019/05/23 /…