美文网首页构建工具:Webpack
Webpack(十二):tree shaking打包性能优化

Webpack(十二):tree shaking打包性能优化

作者: CodeMT | 来源:发表于2020-09-23 10:06 被阅读0次

    1. 什么是tree-shaking?

    webpack中,tree-shaking的作用是可以剔除js中用不上的代码,但是它依赖的是静态的ES6的模块语法。
    也就是说没有被引用到的模块它是不会被打包进来的,可以减少我们的包的大小,减少文件的加载时间,提高用户体验。

    webpack2版本中就开始引入了 tree shaking的概念,它可以在打包时可以忽略哪些没有被使用到的代码。

    注意:要让 Tree Shaking 正常工作的前提是:提交给webpack的javascript代码必须采用了 ES6的模块化语法,因为ES6模块化语法是静态的(在导入,导出语句中的路径必须是静态的字符串)。

    2. 在webpack中如何使用 tree-shaking 呢?

    在配置代码前,我们来看看我们项目中的目录结构如下:

    ### 目录结构如下:
    demo1                                       # 工程名
    |   |--- dist                               # 打包后生成的目录文件             
    |   |--- node_modules                       # 所有的依赖包
    |   |--- js                                 # 存放所有js文件
    |   | |-- demo1.js  
    |   | |-- main.js                           # js入口文件
    |   |--- common                             # js公用的文件
    |   | |-- util.js                           # 公用的util.js文件
    |   |--- webpack.config.js                  # webpack配置文件
    |   |--- index.html                         # html文件
    |   |--- styles                             # 存放所有的css样式文件   
    |   | |-- main.styl                         # main.styl文件   
    |   | |-- index.styl                        
    |   |--- .gitignore  
    |   |--- README.md
    |   |--- package.json
    |   |--- .babelrc                           # babel转码文件
    

    webpack.config.js 代码如下:

    const path = require('path');
    
    // 引入 mini-css-extract-plugin 插件 
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    
    // 清除dist目录下的文件
    const ClearWebpackPlugin = require('clean-webpack-plugin');
    
    const webpack = require('webpack');
    
    // 引入打包html文件
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    // 引入HappyPack插件 
    const HappyPack = require('happypack');
    
    module.exports = {
      // 入口文件
      entry: {
        main: './js/main.js'
      },
      output: {
        filename: '[name].[contenthash].js',
        // 将输出的文件都放在dist目录下
        path: path.resolve(__dirname, 'dist')
      },
      module: {
        rules: [
          {
            // 使用正则去匹配
            test: /\.styl$/,
            use: [
              MiniCssExtractPlugin.loader,
              {
                loader: 'css-loader',
                options: {}
              },
              {
                loader: 'postcss-loader',
                options: {
                  ident: 'postcss',
                  plugins: [
                    require('postcss-cssnext')(),
                    require('cssnano')(),
                    require('postcss-pxtorem')({
                      rootValue: 16,
                      unitPrecision: 5,
                      propWhiteList: []
                    }),
                    require('postcss-sprites')()
                  ]
                }
              },
              {
                loader: 'stylus-loader',
                options: {}
              }
            ]
          },
          {
            test: /\.css$/,
            use: [
              MiniCssExtractPlugin.loader,
              'happypack/loader?id=css-pack'
            ]
          },
          {
            test: /\.(png|jpg)$/,
            use: ['happypack/loader?id=image']
          },
          {
            test: /\.js$/,
            // 将对.js文件的处理转交给id为babel的HappyPack的实列
            use: ['happypack/loader?id=babel'],
            // loader: 'babel-loader',
            exclude: path.resolve(__dirname, 'node_modules') // 排除文件
          }
        ]
      },
      resolve: {
        extensions: ['*', '.js', '.json']
      },
      devtool: 'cheap-module-eval-source-map',
      devServer: {
        port: 8081,
        host: '0.0.0.0',
        headers: {
          'X-foo': '112233'
        },
        inline: true,
        overlay: true,
        stats: 'errors-only'
      },
      mode: 'development',
      plugins: [
        new HtmlWebpackPlugin({
          template: './index.html' // 模版文件
        }),
        new ClearWebpackPlugin(['dist']),
    
        new MiniCssExtractPlugin({
          filename: '[name].[contenthash:8].css'
        }),
        /****   使用HappyPack实例化    *****/
        new HappyPack({
          // 用唯一的标识符id来代表当前的HappyPack 处理一类特定的文件
          id: 'babel',
          // 如何处理.js文件,用法和Loader配置是一样的
          loaders: ['babel-loader']
        }),
        new HappyPack({
          id: 'image',
          loaders: [{
            loader: require.resolve('url-loader'),
            options: {
              limit: 10000,
              name: '[name].[ext]'
            }
          }]
        }),
        // 处理styl文件
        new HappyPack({
          id: 'css-pack',
          loaders: ['css-loader']
        })
      ]
    };
    

    common/util.js 代码如下:

    export function a() {
      alert('aaaa');
    }
    
    export function b() {
      alert('bbbbb');
    }
    
    export function c() {
      alert('cccc');
    }
    

    common/util.js 代码如下:

    export function a() {
      alert('aaaa');
    }
    
    export function b() {
      alert('bbbbb');
    }
    
    export function c() {
      alert('cccc');
    }
    

    js/main.js 代码如下:

    import { a } from '../common/util';
    
    a();
    

    执行 webpack后,打包文件如下:

    然后继续查看 dist/main.xxx.js代码如下:

    "use strict";
    __webpack_require__.r(__webpack_exports__);
    /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return a; });
    /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "b", function() { return b; });
    /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "c", function() { return c; });
    
    
    function a() {
      alert('aaaa');
    }
    
    function b() {
      alert('bbbbb');
    }
    
    function c() {
      alert('cccc');
    }
    
    /***/ }),
    

    如上代码,还是会包含 b,c 两个函数代码进来,那是因为 webpack 想要使用tree-shaking功能的话,我们需要压缩代码,就能把没有引用的代码剔除掉,因此我们需要在webpack中加上压缩js代码如下:

    // 引入 ParallelUglifyPlugin 插件
    const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
    
    module.exports = {
      plugins: [
        // 使用 ParallelUglifyPlugin 并行压缩输出JS代码
        new ParallelUglifyPlugin({
          // 传递给 UglifyJS的参数如下:
          uglifyJS: {
            output: {
              /*
               是否输出可读性较强的代码,即会保留空格和制表符,默认为输出,为了达到更好的压缩效果,
               可以设置为false
              */
              beautify: false,
              /*
               是否保留代码中的注释,默认为保留,为了达到更好的压缩效果,可以设置为false
              */
              comments: false
            },
            compress: {
              /*
               是否在UglifyJS删除没有用到的代码时输出警告信息,默认为输出,可以设置为false关闭这些作用
               不大的警告
              */
              warnings: false,
    
              /*
               是否删除代码中所有的console语句,默认为不删除,开启后,会删除所有的console语句
              */
              drop_console: true,
    
              /*
               是否内嵌虽然已经定义了,但是只用到一次的变量,比如将 var x = 1; y = x, 转换成 y = 5, 默认为不
               转换,为了达到更好的压缩效果,可以设置为false
              */
              collapse_vars: true,
    
              /*
               是否提取出现了多次但是没有定义成变量去引用的静态值,比如将 x = 'xxx'; y = 'xxx'  转换成
               var a = 'xxxx'; x = a; y = a; 默认为不转换,为了达到更好的压缩效果,可以设置为false
              */
              reduce_vars: true
            }
          }
        })
      ]
    }
    

    再运行下打包命令后。我们继续查看代码,如下所示:

    可以看到还是会把无用的 b函数 和 c函数代码打包进去。这是什么情况?那是因为我们在webpack中配置了 mode: 'development',我们现在把它改成 mode: 'production',后,就可以看到只用 a函数了,我们可以到dist目录下的main.js代码内部搜索下 alert, 就可以看到了,只有一个alert('a')了。说明b函数和c函数被剔除掉了。

    tree-shaking 目前的缺陷:

    tree-shaking 能够利用ES6的静态引入规范,减少包的体积,避免不必要的代码引入,但是webpack只能做一点简单的事情。
    比如 我现在在main.js代码改成如下:

    import { func2 } from '../common/util';
    
    var a = func2(222);
    
    alert(a);
    

    common/util.js 代码如下:

    import lodash from 'lodash-es'
    
    var func1 = function(v) {
      alert('111');
      return lodash.isArray(v);
    }
    
    var func2 = function(v) {
      return v;
    };
    
    export {
      func1,
      func2
    }
    

    如上代码,在main.js中引入了 func2, 但是并没有引入func1, 但是func1引入了lodash-es。webpack在检查的时候发现func1中确实用到了lodash-es,因此不会把lodash去掉,但是func1函数会去掉的。但是我们在js中也并没有使用到lodash。因此在这种情况下,webpack中的 tree-shaking 解决不了这种情况,因此 webpack-deep-scope-plugin 插件就可以解决这种问题了,如下没有使用 webpack-deep-scope-plugin 插件打包后的文件大小。如下:


    如上main.js 打包压缩后的js代码大小有81.1kb。打开dist/main.js代码搜索 lodash后,可以搜索到,因此lodash插件被打包进去main.js中了,但是实际上我们项目并没有使用到lodash,因此lodash的库我们按常理来讲并不需要打包进去的。

    3. 使用webpack-deep-scope-plugin 优化

    1. 首先需要安装 webpack-deep-scope-plugin, 安装命令如下:

    npm i -D webpack-deep-scope-plugin
    

    在webpack.config.js 代码引入如下:

    // 引入 webpack-deep-scope-plugin 优化
    const WebpackDeepScopeAnalysisPlugin = require('webpack-deep-scope-plugin').default;
    
    module.exports = {
      plugins: [
        new WebpackDeepScopeAnalysisPlugin()
      ]
    }
    

    然后我们继续打包如下所示:

    打包后发现如上,只有969字节,1kb都不到,再打开dist/main.js 查看代码,搜索下 lodash, 发现搜索不到。

    注意点:
    1. 要使用 tree-shaking,必须保证引用的插件的模块是ES6模块规范编写的,这也是我为什么引用了的是 lodash-es,而不是 'lodash', 如果引用的是lodash的话,是不能去掉的。

    2. 在 .babelrc 中,babel设置 module: false, 避免babel将模块转换为成 CommonJS规范。引入模块包也必须符合ES6规范的。如下 babelrc代码:

    {
      "plugins": [
         [
          "transform-runtime",
          {
            "polyfill": false
          }
         ]
       ],
       "presets": [
         [
           "env",
           {
             "modules": false   // 关闭Babel的模块转换功能,保留ES6模块化语法
           }
         ],
         "stage-2"
      ]
    }
    

    且需要在 package.json 中定义 sideEffect: false, 这也是为了避免出现 import xxx 导致模块内部的一些函数执行后影响全局环境, 却被去除掉的情况.

    3. webpack-deep-scope-plugin 插件依赖 node8.0+ 和 webpack 4.14.0 +

    相关文章

      网友评论

        本文标题:Webpack(十二):tree shaking打包性能优化

        本文链接:https://www.haomeiwen.com/subject/uiwphktx.html