directory

  • Introduction to the
  • Configuration items for using tree Shaking
  • Theoretical basis of Tree Shaking function
  • How is Tree Shaking implemented
  • At the end

Introduction to the

If you have used WebPack, you will not be unfamiliar with tree Shaking. Tree shaking, usually used to remove dead-code from a javascript context.

Tree Shaking, can effectively reduce the size of the final package file. Therefore, it is a common optimization method to enable Tree shaking in webPack packaging process.

Configuration items for using tree Shaking

To use WebPack’s Tree Shaking feature, we need to do some configuration first.

Webpack provides two levels of tree shaking functionality: modules-level and declaration-level. Different levels of Tree shaking correspond to different configuration items.

  • modules-level

    Modules-level: Tree Shaking applies to the entire module. If a module is referenced but not used, it does not appear in the final package code.

    The example code is as follows:

    Js export default function funA() {console.log('funcA')} // index.js import funcA from './example.1.js'; console.log('index');Copy the code

    After the package, the code looks like this, in which example.1 module is removed:

    // bundle.js (self["webpackChunkwebpack_treeshaking"] = self["webpackChunkwebpack_treeshaking"] || []).push([[179],{ /***/ "./index.js": /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); console.log('index'); /***/ }) // }, 0,[["./index.js",303]]]);Copy the code

    When using the Modules-level Tree shaking function, we need to configure it as follows:

    1. First, we need to set optimization.sideEffects to true;

      The value of Optimization. SideEffects is true, which means webPack’s Tree Shaking feature is turned on. This property defaults to true in Production mode and does not need to be configured.

    2. Second, we need to set the sideEffects property in the package.json file to false or do nothing;

      Setting the sideEffects property to false means that webpack will assume that there are no sideEffects when using the tree shaking function, and that it is safe to remove unused modules. If there is no sideEffects property or if the sideEffects property value is true, webpack will analyze tree shaking itself for sideEffects and remove unused modules if there are no sideEffects.

  • statements-level

    Statements – Level the level at which the Tree Shaking function is applied to statements within the module. If an export defined internally in a module is not referenced, or is referenced but not used, then the export will not appear in the final packaging code.

    The example code is as follows:

    // example.2.js
    export const funcB = () => { console.log('funcB') }
    export const funcC = () => { console.log('funcC') }
    
    // index.js
    import { funcB, funcC } from './example.2.js';
    funcC();    
    Copy the code

    The package code looks like this, where the func of the example.2 module is removed:

    // bundle.js (self.webpackChunkwebpack_treeshaking=self.webpackChunkwebpack_treeshaking||[]).push([[179], { "./index.js":(e,s,c)=>{"use strict";(0,c("./example.2.js").I)()}, ". / example. 2. Js "(e, s, c) = > {" use strict"; c.d (s, n} {: (I) = >); const n = () = > {the console. The log (" funcC ")} / / funcB have been removed} },0,[["./index.js",303]]]);Copy the code

    To use the Tree shaking function of declaration-level, follow these steps:

    1. First, we need to set the optimization.usedExports attribute to true(in production mode, the default is true).

    2. Secondly, we need to set the optimization. Minimize property value to true(the default is true in production mode);

In the actual project, we will enable the tree shaking function of modules-level and waste-level at the same time, remove all unused modules and the unused export inside modules, and reduce the size of the package file.

Theoretical basis of Tree Shaking function

After understanding the configuration items for tree Shaking, let’s take a look at the theoretical basis for tree Shaking.

As anyone who has worked with WebPack knows, for tree Shaking to work, we must use es6-import to reference modules. If the module is referenced in common.js-require mode, the tree shaking function is invalid.

Why is it that the es6-import reference module uses tree shaking, but the common.js-require reference module does not?

To answer this question, we need to know two things first: how js code is executed and the difference between ES6-Module and Commonjs-Module.

  • Js code execution process

    About V8 engine is how to execute a JS code, there have been a lot of explanation on the Internet, you can search. Here, I recommend an article by Geek Time teacher Li Bing – Compilers and Interpreters: How V8 executes a piece of JavaScript code.

    In general, when a piece of JS code is executed, it goes through the following steps:

    1. Source code generates abstract syntax tree (AST) and execution context through syntax analysis and lexical analysis;

    2. Generate bytecode from an abstract syntax tree (AST);

    3. Execute the generated bytecode;

  • Comparison between es6-module and Commonjs-module

    Es6-module and commonjs-module are two commonjs module solutions.

    The design idea of ES6-module is to be as static as possible, so that the dependency between modules and the output of modules can be determined during the compilation phase of JS code. Unlike Commonjs-Module, we only know the output of the module when the JS code is actually executed.

    Es6-module will know the export of the dependent module at the end of the first step, while Commonjs-module will know the export of the dependent module at the end of the third step.

To sum up, it is based on the fact that JS code needs to be compiled before execution, and es6-module can determine the dependency between modules and depend on the output of modules when JS code is compiled that WebPack can statically parse the content of source files during the packaging process. Determine the dependencies between modules and the used exports of the modules, and then remove the unused modules and the unused exports in the modules to achieve the purpose of tree shaking.

How is Tree Shaking implemented

Now that we know the theoretical basis for Tree Shaking, it’s time to look at how WebPack implements tree Shaking functionality.

Webpack processes the source files of our project into the final package, going through the process of building the module dependency diagram, encapsulating the module dependency diagram into chunks, building the content for the chunks, and exporting the content for the chunks to the specified location.

Tree shaking occurs during the process of encapsulating chunks and building the content corresponding to the chunks.

In order to explain tree Shaking more vividly, this article will use a simple example to comb through the implementation process of Tree Shaking.

The example code is as follows:

// example.1.js

export default function funcA() {
    console.log('funcA');
}

Copy the code
// example.2.js

export const funcB = () => {
    console.log('funcB');
}

export const funcC = () => {
    console.log('funcC');
}

export const funcD = () => {
    console.log('funcD');
}

export const funcE = () => {
    console.log('funcE');
}
Copy the code
// example.3.js
import { funcD } from './example.2';
import funcA from './example.1';

export const funcF = () => {
    funcD();
    funcA();
    console.log('funcF');
}

export const funcH = () => {
    console.log('funcH');
}
Copy the code
// example.4.js
export const funcG = () => {
    console.log('funcG');
}
Copy the code
// main.js
import funcA from './example.1';
import { funcG } from './example.4';
import { funcB } from './example.2';
import(/* webpackChunkName: "example.3" */'./example.3').then((module) => {
    console.log('123');
});
funcB();
Copy the code

The corresponding WebPack configuration is as follows:

const config = { mode: 'production', entry: path.resolve(__dirname, '.. /index'), optimization: { concatenateModules: false, minimize: true, runtimeChunk: true, usedExports: true, moduleIds: 'named', sideEffects: true, } };Copy the code

Build module dependency diagrams

First, let’s look at building a module dependency graph.

In the process of compilation and packaging, WebPack will recursively build a module dependency graph according to the dependency relationship between each module in the project. The specific process is as follows:

For each module in the example, the corresponding module dependency diagram is as follows:

In the figure, we find that the dependencies between modules are determined by three types of edges:

  • HarmonyImportSideEffectDependency

    HarmonyImportSideEffectDependency used to represent the static reference relationship between modules.

    In this example, the main module uses es6-import to reference the example.1, example.2, and example.4 modules. The webpack will for example. 1, example. 2, example. 4 create a dependency HarmonyImportSideEffectDependency type object, Add to the Dependencies list of the main module.

  • HarmImportSpecifierDependency

    HarmonyImportSpecifierDependency used to represent the module used to export.

    Example, the main module USES the example. 2 modules provide funB webpack will for example. 2 create a dependency HarmImportSpecifierDependency type object, Add to the Dependencies list of the main module.

  • AsyncDependenciesBolock

    AsyncDependenciesBolock is used to indicate the module to be dynamically loaded.

    Webpack will create an AsyncDependenciesBolock dependency object for example.3, which is lazy loaded by the main module. Add to the blocks list of the main module.

Module dependency graph preprocessing

After the module dependency diagram is built, the next step is to build chunks based on the module dependency diagram.

However, webPack also needs to preprocess the module dependency diagram before building chunks.

During preprocessing, WebPack does the following:

  • Determine the usedExports of each module

    The usedExports of each module represent the export used by the module. Webpack can remove the unused export of a module only after the usedExports of each module are determined.

    During pretreatment, webpack is based on the module dependency graph HarmImportSpecifierDependency usedExports type of edge to determine for each module.

    Module dependency graph of each HarmImportSpecifierDependency edge, module by using the export corresponds to the dependency.

    Example, the main modules and the example. 4 no HarmImportSpecifierDependency type of edge between modules, illustrate the main module only cited example. 4, but the actual is not used in the example. The default output, 4 Example.4 usedExports is undefined. And the main modules and the example. 2 points to funcB HarmImportSpecifierDependency edge between modules, example. 3 modules and example. Have to funcD between two modules FuncB HarmImportSpecifierDependency edge, the example. 2 block, funcD is used again, the example. 2 modules usedExports funcB and funcD.

    The sample module dependency diagram after processing is as follows:

    To determine the usedExports of each module, the value of the optimisation. usedExports attribute is true. If optimism. usedExports is false, then the usedExports for each module cannot be determined, and WebPack cannot remove unused exports. In production mode, the value of optimization.usedExports is true.

  • Remove unused modules

    After determining the actual output of each module, WebPack next removes the unused modules from the module dependency graph.

    Module can be removed, whether can pass between modules and HarmonyImportSideEffectDependency HarmImportSpecifierDependency side to determine the type. If only HarmonyImportSideEffectDependency type of boundary between modules, then the corresponding dependent module can be removed.

    Observation example module dependency graph, the main modules and example. 4 only HarmonyImportSideEffectDependency type of boundary between modules, no HarmImportSpecifierDependency types of edges, Example.4 is referenced only by the main module, and its output is not used by the main module. Therefore, examale. And the main modules and the example. 2 between modules, both HarmonyImportSideEffectDependency type, also have HarmImportSpecifierDependency type of edge, Example.2 will not be deleted if its output is used by the main module.

    The module dependency diagram after processing is as follows:

    To remove an unused module from export, you need to set the value of optimization. SideEffects to true. If optimization.sideEffects is false, export unused modules will not be removed. In production mode, optimization. SideEffects defaults to true.

Encapsulation chunks

After the preprocessing is complete, WebPack then encapsulates the module dependency diagram into chunks. Webpack will traverse the module dependency diagram to find the edge of type AsyncDependenciesBolock in the module dependency diagram and then split the module dependency diagram into chunks.

There is an AsyncDependenciesBolock side between the main module and the example.3 module. Then WebPack will split the module dependency graph into main and example.3 chunks based on the AsyncDependenciesBolock edge. The main chunk contains modules main and example.2, and the example.3 chunk contains modules example.3 and example.1.

The example shows the following chunks:

Building chunk content

After the chunks build is complete, the next step for WebPack is to build the output for each chunk. Webpack builds content for each chunk’s modules, and then generates the chunk’s content based on the module’s content. When building the contents of the module, the value of usedExports (that is, the value of the optimization. UsedExports configuration item) affects the final result.

In this example, the example.2 module is constructed as follows based on usedExports configuration items:

  • optimization.usedExports: false

    In example.2, usedExports is null, and the build content is:

    "./example.2.js":((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, { "funcB": () => /* binding */ funcB, "funcC": () => /* binding */ funcC, "funcD": () => /* binding */ funcD, "funcE": () => /* binding */ funcE }); const funcB = () => { console.log('funcB'); } const funcC = () => { console.log('funcC'); } const funcD = () => { console.log('funcD'); } const funcE = () => { console.log('funcE'); }})}Copy the code
  • optimization.useExports: true

    In example.2, usedExports are funcB and funcD, and the constructed content is:

    "./example.2.js":((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, { "funcB": () => /* binding */ funcB, "funcD": () => /* binding */ funcD }); const funcB = () => { console.log('funcB'); } const funcC = () => { console.log('funcC'); } const funcD = () => { console.log('funcD'); } const funcE = () => { console.log('funcE'); }})}Copy the code

In the build code above, webpack_exports corresponds to example.2 module in the actual application. If optimity.usedexports is false, then __webpack_exports__ contains all the exports defined in example.2, If optimity.usedexports is true, __webpack_exports__ contains the export defined in Example.2 and is used.

After the chunk content is built, if we set the minimize property to true in the configuration item, WebPack will enable Terser to compress the built content, obvert it, and remove unused code.

Terser also parses the processed content into an AST object, and then analyzes the AST object to remove unused code from the module.

In production mode, the default is true.

In the example.2 module, __webpack_exports__ contains funcB and funcD when optimity.usedexports is true, and funcA and funcE are not actually used, So funcA and funcE are removed by terser. Thus, example.2 completes the tree-shaking of declaration-level.

Finally, the webPack is packaged by exporting the builds for each chunk to the location specified by the Output configuration item.

The result of the package is as follows:

// main.js
(self.webpackChunkwebpack_treeshaking=self.webpackChunkwebpack_treeshaking||[]).push([[179],
    {
        "./index.js":(e,s,c)=>{
            "use strict";
            var n=c("./src/example.2.js");
            c.e(394).then(c.bind(c,"./src/example.3.js")).then((e=>{console.log("123")})),(0,n.Ii)()
        },
        "./src/example.2.js":(e,s,c)=>{
            "use strict";
            c.d(s,{Ii:()=>n,A_:()=>l});
            const n=()=>{console.log("funcB")},l=()=>{console.log("funcD")}
        }
    },0,[["./index.js",303]]]);
Copy the code
// example.3.js
(self.webpackChunkwebpack_treeshaking=self.webpackChunkwebpack_treeshaking||[]).push([[394],{
    "./src/example.1.js":(e,s,c)=>{
        "use strict";
        function n(){console.log("funcA")}
        c.d(s,{Z:()=>n})
    },
    "./src/example.3.js":(e,s,c)=>{
        "use strict";
        c.r(s),
        c.d(s,{funcF:()=>a,funcH:()=>o});
        var n=c("./src/example.2.js"),l=c("./src/example.1.js");
        const a=()=>{(0,n.A_)(),(0,l.Z)(),console.log("funcF")},o=()=>{console.log("funcH")
    }
}}]);
Copy the code

At the end

Here, I believe that you have the whole realization principle and process of Tree Shaking, have a clearer understanding of it.

Finally, let’s make a conclusion:

  • Tree shaking has two levels: modules-level and declaration-level. Modules-level removes unused modules, and assembly-level removes unused export of the module.

  • Modules-level requires setting optimization.sideEffects to true;

  • Statements -level requires the configuration optimization. UsedExports, optimization. Take care to be true;

  • Modules-level and statement-level do not affect each other.

  • You must use es6-import to reference modules, otherwise tree shaking will not work.