webpack之 tree shaking

文章目录

      • tree shaking
      • SideEffectsFlagPlugin
      • ModuleConcatenationPlugin
      • TerserWebpackPlugin
      • 参考文章

tree shaking

//math.js
export function square(x){
     
    return x*x;
}

export function cube(x){
     
    return x*x*x;
}
//index.js
import {
     cube} from './math.js';

const element = document.createElement('pre');
element.innerHTML = '5 cubed is equal to ' + cube(5);
document.body.appendChild(element);
//webpack.config.js
//webpack verson:^5.0.0-beta.17
 const path = require('path');
 const HtmlWebapckPlugin = require("html-webpack-plugin");
 const {
     CleanWebpackPlugin} = require("clean-webpack-plugin");

 module.exports = {
     
    mode:'development',
    devtool:'cheap-source-map',
    // mode:'production',
     devServer:{
     
         port:3000,
        contentBase:path.join(__dirname,'dist')
     },
     entry:"./src/index.js",
     output:{
     
         filename:"bundle.js",
         path:path.join(__dirname,"dist")
     },
     module:{
     
         rules:[
             {
     
                 test:/\.js$/,
                 include:/src/,
                 exclude:/node_modules/,
                 use:{
     
                     loader:'babel-loader',
                     options:{
     
                        //  presets:['@babel/preset-react']
                        // presets:[
                        //     [
                        //         '@babel/preset-env',{
     
                        //             modules:false
                        //         }
                        //     ]
                        // ]
                     }
                 }
             },
             {
     
                 test:/\.css$/,
                 include:/src/,
                 use:['style-loader','css-loader']
             }
         ]
     },
     plugins:[
         new HtmlWebapckPlugin({
     
             template:'./index.html'
         }),
         new CleanWebpackPlugin()
     ]
 }

webpack会对代码作如下标记:

  • 所有import标记为 harmony import
  • 引用到的export标记为harmony export,没引用到的export标记为unused harmony export
    本例中,math.js导出了squarecube,但index.js中只使用了cube,没有使用square,所以math.jscubeharmony exportsquareunused harmony export
    webpack之 tree shaking_第1张图片
    在这里插入图片描述

math.js导出了squarecube,而square并没有被使用。说白了,square多余了,完全没有必要打包到bundle.js里,否则徒增代码体积。
mode设置为productionwebpack在打包过程中会将 没有使用的导出内容 排除出最终包。 以上过程就是 tree shaking
webpack之 tree shaking_第2张图片

有两个疑问:

  • 首先,为什么只有 ES Moduleimport/export)下才可以 tree shaking?
    构建时通过静态分析,分析出代码之间的依赖关系。把我们的代码想象成一棵树,各依赖项就是树上的节点,把无用的依赖项从构建结果中删除,就是tree shaking
    要静态分析,代码就不能变来变去。像有 动态导入,按需加载的代码就没有办法做静态分析。
    ES Module的设计思想就是尽量使代码静态化,使得在编译时就可以确定依赖关系,而不是非得到运行时才确定。
  • 然后,为什么 mode:'production'就实现了 tree shaking?webpack做了啥?
    • DefinePluginprocess.env.NODE_ENV 的值设置为 production
      相当于new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") })
    • 启用了SideEffectsFlagPlugin
    • 启用了ModuleConcatenationPlugin
    • 启用了TerserWebpackPlugin

其中,SideEffectsFlagPlugin+ModuleConcatenationPlugin
没有使用的且没有副作用的模块排除出包(移除未使用的模块)、TerserPlugin将没有使用的代码语句排除出包(移除未使用的代码)

SideEffectsFlagPlugin

先举个例子来了解下 SideEffects,副作用。
新增一个模块文件util.js

export function getDate(){
     
    return new Date().toLocaleDateString();
}

index.js里导入util.jsgetDate方法,但不调用。

//index.js
import {
     cube} from './math.js';
import {
     getDate} from './util.js'
const element = document.createElement('pre');
element.innerHTML = '5 cubed is equal to ' + cube(5);
document.body.appendChild(element);

mode:'production',构建的结果里没有util.js的任何痕迹。也就是说,没有被使用的且没有副作用的模块util.js被移除了。
webpack之 tree shaking_第3张图片
现在,在util.js中添加一句console.log('hello world'),即

//util.js
export function getDate(){
     
    return new Date().toLocaleDateString();
}
console.log('hello world');

编译后的结果里,从util.js导入的getDate方法因为没有被调用,删除肯定不影响功能,所以直接移除就好。但util.js里的console.log('hello world'),编译器就无法判断这句到底是干嘛的,保险起见,当然最好不移除。也就是说,console.log('hello world')被编译器认定为有副作用了,有副作用的东西不能删除。
webpack之 tree shaking_第4张图片
还有像 修改全局变量如window.name='testing'等等 都是 副作用。
与 副作用 相反的就是 纯函数。像reactUI=render(data),就是一个纯函数。 输出UI完全依赖输入data,只要输入相同,输出就一定相同。

再回到 副作用的 这个例子。
console.log('hello world'),编译器不知道它是干嘛的,但我们知道啊。我们可以告诉webpack这句明显是没啥用,完全可以删除它。
怎么告诉?在package.json里添加"sideEffects":false
"sideEffects":false告诉webpack,所有的代码都是没有副作用的,没有使用到的你放心删掉就好了。
webpack之 tree shaking_第5张图片
"sideEffects":false是不是很妙?不见得。
我们再在index.js导入index.css

//index.css
body{
     
    font-weight:bold;
    color:indianred;
}
//index.js
import {
     cube} from './math.js';
import {
     getDate} from './util.js';
import './index.css';
const element = document.createElement('pre');
element.innerHTML = '5 cubed is equal to ' + cube(5);
document.body.appendChild(element);

情况不妙,样式并没有加上。因为index.css是没有被使用的代码,且"sideEffects":false后它已经被webpack认定是无副作用的,所以index.css被移除了。
怎么破?有两种方式。

  • 第一种,依然得靠package.json里的sideEffects属性
    "sideEffects":["*.css"]告诉webpack,所有的.css文件都是有副作用的,构建时不要移除
//package.json
"sideEffects":["*.css"]
  • 第二种,可以在module.rule里设置sideEffects,但请注意,是布尔值
//webpack.config.js
{
     
    test:/\.css$/,
    include:/src/,
    use:['style-loader','css-loader'],
    sideEffects:true
}

好了,我们已经了解副作用 以及 模块有无副作用该如何标注。
SideEffectsFlagPlugin则会分析模块之间的依赖关系,没有使用且没有副作用的模块将被打上 无副作用的 标记。

ModuleConcatenationPlugin

它的作用是作用域提升,更直白一点讲,是将多个模块合并到一个模块里。
开启该功能有两种方式。

//第一种
    plugins:[
        new webpack.optimize.ModuleConcatenationPlugin()
    ]
//第二种
    optimization:{
     
       concatenateModules :true
    }

mode:'development'devtool:'cheap-source-map'下, ModuleConcatenationPlugin开启前后的对比。
webpack之 tree shaking_第6张图片
不启用ModuleConcatenationPlugin:各个模块独占一个闭包;
启用ModuleConcatenationPlugin:所有模块占一个闭包。
最直观的一个感受是,启用ModuleConcatenationPlugin后,代码体积变小了些。模块越多,感受越明显。

SideEffectsFlagPlugin会给 没有被使用且没有副作用的模块打上 无副作用的 标记,而这些被打上标记的模块会被ModuleConcatenationPlugin跳过,不被合并入最终模块,即被排除出最终包。

TerserWebpackPlugin

TerserWebpackPlugin会使用terser压缩js。

以前会用插件UglifyjsWebpackPlugin,它会使用uglifyjs压缩js,但因为uglifyjs仅支持ES5,而terser支持ES6+,所以现在用TerserWebpackPlugin替代UglifyjsWebpackPlugin

前面sideEffects+SideEffectsFlagPlugin+ModuleConcatenationPlugin从模块层面上移除了 没有使用且没有副作用的代码。
TerserPlugin从代码语句层面上移除了 没有副作用的代码。
/*#__PURE__*/将函数调用标识为无副作用
还是看例子。

export function getDate(){
     
    return new Date().toLocaleDateString();
}

function sayHi(){
     
    console.log('hello world');
}
sayHi();

mode:'production'编译后的结果里有console.log('hello world')
webpack之 tree shaking_第7张图片
现在,我们在sayHi函数调用前添加这么一行注释/*#__PURE__*/,`即

export function getDate(){
     
    return new Date().toLocaleDateString();
}

function sayHi(){
     
    console.log('hello world');
}

/*#__PURE__*/sayHi();

/*#__PURE__*/将函数调用标识为无副作用。
瞧,无副作用的sayHi()被移除了。
webpack之 tree shaking_第8张图片
pure_funcs将函数标记为无副作用
除了/*#__PURE__*/这个方法外,还可以用pure_funcssayHi标记函数为无副作用。


const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
     
   devtool:'cheap-source-map',
   mode:'production',
    optimization: {
     
        concatenateModules:true,
        minimize: true,
        minimizer:[
            new TerserPlugin({
     
                terserOptions:{
     
                    compress:{
     
                        pure_funcs:['sayHi'],
                        drop_console:true
                    }
                }
            })
        ]        
    }
}

terserOptions.compress对象里可以设置很多属性,可以到这里看看。
在开发阶段我们可能会使用很多console.log打印日志,但发布的时候肯定要删除这些,将terserOptions.compress.drop_console设置为true就好。

参考文章

Tree Shaking in Webpack
代码体积减少80%!Taro H5转换与优化升级
UglifyjsWebpackPlugin
TerserWebpackPlugin
UglifyJS
terser

你可能感兴趣的:(webpack)