看完这篇你一定懂 --- Webpack4 打包优化

前面的话

Webpack升级4之后,小柒踩过的很多坑,这篇文章总结Webpack4的一些新特性,以及常见的优化方式。

Webpack4 新特性

  • 不再强制需要webpack.config.js配置文件。默认入口为./src/index.js,默认输出./dist目录,输出文件main.js

  • Webpack不再能单独使用,4.x版本的很多命令都移动到了webpack-cli中。所以必须安装webpackwebpack-cli

  • 开箱即用 WebAssembly,webpack4提供了wasm的支持,现在可以引入和导出任何一个 Webassembly 的模块,也可以写一个loader来引入C++、C和Rust。(注:WebAssembly 模块只能在异步chunks中使用)

  • 增加模式区分,在配置文件中使用mode选项来配置相应的模式:
    development:开发模式。打包默认不压缩代码,默认开启代码调试
    production:生产模式,线上使用。打包压缩代码,不开启代码调试

  • 一些插件由optimeztion配置代替。
    比如:

    • CommonsChunkPlugin废弃,由optimization.splitChunksoptimization.runtimeChunk代替,前者拆分代码,后者提取runtime代码
    • optimize.UglifyJsPlugin 废弃,由 optimization.minimize 替代,生产环境默认开启。
  • 升级到 webpack4 后,mini-css-extract-plugin 替代 extract-text-webpack-plugin 成为css分离首选。

量化

speed-measure-webpack-paligin插件可以测量各个插件和loader所花费的时间。
使用如下:

 const SpeedMeasurePlugin = require('speed-measure-webpack-palugin');
 const smp = new SpeedMeasurePlugin();
 const config = {
 	// ...webpack配置
}
module.exports = smp.wrap(config);

在这里插入图片描述

从打包时间上优化

从哪几个方面优化
  • 减少搜索依赖的时间
  • 减少loader解析时间
  • 减少js文件压缩时间: 将所有解析完的代码,打包到一个文件中,为了减小白屏时间,webpack为对其进行优化。js压缩是发布编译的最后阶段,通常webpack需要卡好一会,因为压缩js代码要将代码解析成AST,再根据复杂的规则去分析和处理AST,最后将AST还原为js。
  • 减少二次打包时间:当我们更改项目中的文件是,就要重新打包。有些文件其实不需要再次打包,所以要减少二次打包时间。
1、减少搜索依赖的时间
  • 优化loader配置
    由于loader转换文件比较耗时,所以让尽可能少的文件被loader处理。使用testincludeexclude三个配置项来命中loader要应用的文件。
  • 优化resolve.modules配置
    resolve.modules用于配置Webpack去哪些目录下寻找第三方模块。resolve.modules的默认值是['node_modules'],意思是先从当前目录下的./node_modules目录下去找我们想要的模块,如果没有就去上级目录…/node_modules,再没有就去…/…/node_modules中找,以此类推。
  • 优化resolve.mainFields配置
    用于配置第三方模板使用哪个入口文件。
  • 优化resolve.alias配置
    通过别名来把原来的导入路径映射成为一个新的路径,减少解析时间
  • 优化reslove.extensions配置
    在导入语句没有文件后缀时,Webpack会根据resolve.extension自动带上后缀后去尝试询问文件是否存在。
  • 优化module.noParse配置
    可以让Webpack忽略对部分模块没有采用模块化的文件的递归解析处理。比如:jQuery、ChartJS它们庞大有没有采用模块化标准,让Webpack去解析这些文件耗时又没有意义。

详细配置:根据你的项目去选择

// 编译代码的基础配置
module.exports = {
  // ...
  module: {
    // 项目中使用的 jquery 并没有采用模块化标准,webpack 忽略它
    noParse: /jquery/,
    rules: [
      {
        // 这里编译 js、jsx
        // 注意:如果项目源码中没有 jsx 文件就不要写 /\.jsx?$/,提升正则表达式性能
        test: /\.(js|jsx)$/,
        // babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
        use: ['babel-loader?cacheDirectory'],
        // 排除 node_modules 目录下的文件
        // node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
        exclude: /node_modules/,
      },
    ]
  },
  resolve: {
    // 设置模块导入规则,import/require时会直接在这些目录找文件
    // 可以指明存放第三方模块的绝对路径,以减少寻找
    modules: [
      path.resolve(`${project}/client/components`), 
      path.resolve('h5_commonr/components'), 
      'node_modules'
    ],
    // import导入时省略后缀
    // 注意:尽可能的减少后缀尝试的可能性
    extensions: ['.js', '.jsx', '.react.js', '.css', '.json'],
    // import导入时别名,减少耗时的递归解析操作
    alias: {
      '@compontents': path.resolve(`${project}/compontents`),
    }
  },
};
2、 减少loader的解析时间(开启多进程)

webpack是单线程模式,只能一个一个文件去处理,当打包文件比较大时,打包时间就会比较长。

  • HappyPack(Webpack 3)

    原理:将loader的解析交给多个进程并行去处理,发挥CPU多核的能力,从而减少构建时间。
    安装依赖: npm install happypack -D
    使用如下:

    // 比如对js文件的处理:
    // 引入happypack
    const HappyPack = require('happypack'); 
    // 引入系统操作模块
    const os = require('os');
    // 构造共享进程池,根据系统内核数量,指定进程池的个数,也可以是其他数量
    const happyThreadPool = HappyPack.ThreadPool({size: os.cpus().length}); 
    const createHappyPlugin = (id, loaders) => new HappyPack({
      id: id, // 唯一标识
      loaders: loaders, // 使用的loader
      // 其他选项
      threadPool:  happyThreadPool,  //使用共享进程池中的子进程去处理任务
      verbose: true // 是否允许HappyPack输出日志,默认为true
    })
    module.exports = {
     // ... 其他选项
     module: {
        rules: [
            // ... 其他文件loader配置
            {
              test: /\.js$/,
              // 对js文件的处理交给`id`为babel的HappyPack实例
              use: ['happypack/loader?id=babel'],
              // node_modules目录下的文件都是es5语法,不用通过babel转换
              exclude: /node_modules/,
            },
            
          ]
      },
      plugins: [
        // ...其他插件
        createHappyPlugin('babel',[{
          loader: 'babel-loader',
          options: {
            babelrc: true,
            cacheDirectory: true // 启用缓存
          }
        }])
      ]
    }
    

    注意:只在解析时间长的loader上使用,项目小也不必使用。并且happypack不推荐使用,现在已经不再维护它

  • thread-loader(Webpack4 推荐)

    使用比较简单,将这个thread-loader放在其他loader之前就可以了。

    原理: 也是开启多进程来并行loader的解析,被放置了thread-loader的loader就会在单独的worker池中运行,一个worker就是一个node进程。每个单独的进程处理时间限制在600ms。

    安装依赖: npm install thread-loader -D

    使用如下:

    module.exports = {
      // ...
      module: {
        rules: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            // 创建一个 js worker 池
            use: [ 
              'thread-loader',
              'babel-loader'
            ] 
          },
          {
            test: /\.s?css$/,
            exclude: /node_modules/,
            // 创建一个 css worker 池
            use: [
              'style-loader',
              'thread-loader',
              {
                loader: 'css-loader',
                options: {
                  modules: true,
                  localIdentName: '[name]__[local]--[hash:base64:5]',
                  importLoaders: 1
                }
              },
            ]
          }
          // ...
        ]
        // ...
    
3、减少js压缩时间

发布到到线上的代码,一般都会压缩js代码。

  • UglifyJsPlugin (Webpack 3)

    UglifyJsPlugin是webpack3内置的的插件,使用时引入就好,是单进程的。

    原理: 配置在压缩过程中使用的规则。

    使用如下,给出最优化的代码配置:

    const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
    
    module.exports = {
     // ...
     plugins: [
      new UglifyJsPlugin({
          compress: {
            warnings: false,// 在UglifyJsPlugin删除没有用到的代码时,不是输出警告
            drop_console: true,// 删除所有的'console'语句
            collapse_var: true,// 内嵌已定义,但是只用了一次的变量
            reduce_var: true,// 提取出现了多次但没有定义成变量去引用的静态值。
          },
          output:{
            // 最紧凑的输出
            beautify: false,
            // 删除所有的注释
            comments: false
          }
        })
     ]
    

    此外,Webpack提供了更简单的方法来接入UglifyJsPlugin,直接在启动Webpack时,带上--optimize-minimize参数。即webpack --optimize-minimize

  • webpack-parallel-uglify-plugin (Webpack 3)

    看名字都知道,并行的UglifyPlugin嘛,就是利用了多进程

    原理:开启多个子进程,对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过UgilifyJS去压缩代码,但是并行执行。子进程处理完后再把结果发送给主进程。

    安装依赖: npm install webpack-parallel-uglify-plugin -D

    使用如下:

    const ParallelUglifyJsPlugin = require('webpack-parallel-uglify-plugin');
    
    	
    	module.exports = {
    	 // ...
    	 plugins: [
    	   new ParallelUglifyJsPlugin({
    	   	 cacheDir: './cache' // 缓存压缩后的结果
    	      // 传递给UglifyJS的参数
    	      uglifyJS: {
    	        compress: {
    	          warnings: false,// 在UglifyJsPlugin删除没有用到的代码时,不是输出警告
    	          drop_console: true,// 删除所有的'console'语句
    	          collapse_var: true,// 内嵌已定义,但是只用了一次的变量
    	          reduce_var: true,// 提取出现了多次但没有定义成变量去引用的静态值。
    	        },
    	        output:{
    	          // 最紧凑的输出
    	          beautify: false,
    	          // 删除所有的注释
    	          comments: false
    	        }
    	      }
    	})
     ]
    

    上面的ugilifyJS参数是用于压缩ES5代码是时的配置,也可以是ugilifyES:用于压缩ES6代码。

  • terser-webpack-plugin(Webpack 4)

    terser:简要的。

    官方定义:用于ES6+的js解析器、压缩工具。为何选择terser:不再维护UglifyES,并且UgilifyJS不支持ES6+。terser是UglifyES的一个分支,主要保留了与UglifyJS和UglifyES的API和CLI兼容性。

    原理:开启多进程

    安装依赖: npm install terser-webpack-plugin -D
    使用如下:

    module.exports = {
      optimization: {
        minimizer: [
          new TerserPlugin({
            parallel: true,
          }),
        ],
      },
    };
    
4、 减少二次打包时间

合理利用缓存,来减少二次打包时间。比如cache-loader HardSourceWebpackPluginbabel-loader的cacheDirectory标志。但所有的缓存方法都有启动开销。二次打包节约时间,初次打包很慢

  • cache-loader

    cache-loader的配置很简单,只要放在其他loader之前即可。如果只是下babel-loader配置cache,使用babel-loadercacheDirectory

    安装依赖npm install cache-loader -D

    module.exports = {
      module: {
        rules: [
          {
            test: /\.ext$/,
            use: ['cache-loader', ...loaders],
            include: path.resolve('src'),
          },
        ],
      },
    };
    
  • hard-source-webpack-plugin

    为模块提供中间缓存。缓存默认的存放路径是:node_modules/.cache/hard-source.

    使用如下:

    const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
    // ...
    plugins: [
        new HardSourceWebpackPlugin()
      ],
    

    非首次使用HardSourceWebpackPlugin打包的时间,明显比没有使用要快:
    在这里插入图片描述

从打包体积上优化

  • Tree Shaking (依赖需要采用ES6模块化语法的代码)

    可以去除无用代码。

    要求:必须使用ES6模块化语法,使用babel时,要关闭其模块转换功能,修改.babelrc文件或者在 webpack.config.js 配置文件中将modules设置为false。

    // .babelrc
    {
      "presets": [
        ["env",
          {
            "modules": false // 保留es6的模块化语法
          }
        ]
      ]
    }
    

    Webpack4下的配置:

    • 首先,你必须处于生产模式。Webpack 只有在压缩代码的时候会 tree-shaking。
    • 其次,开启优化选项 “usedExports” 。这样Webpack就看可以提示你哪些是用不上的代码
    • 最后,你需要使用一个支持删除死代码的压缩器。上面提到过terser-webpack-plugin

    注意:生产环境下默认开启

    const config = {
     mode: 'production', // 生产模式
     optimization: {
     usedExports: true, // 
     minimizer: [
      new TerserPlugin({...})
     ]
     }
    };
    

    还需注意的一点:

    package.json文件中,有一个特殊的属性sideEffects,它有三个可能值:

    • true: 是默认值,意味着所有的文件都不能进行tree-shaking

    • false: 表示所有的文件都可以进行tree-shaking

    • [...]: 表示在数组中的文件,是不可以进行tree-shaking的,其他文件都可以.

      // 所有文件都有副作用,全都不可 tree-shaking
      {
       "sideEffects": true
      }
      // 没有文件有副作用,全都可以 tree-shaking
      {
       "sideEffects": false
      }
      // 只有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件
      {
       "sideEffects": [
       "./src/file1.js",
       "./src/file2.js"
       ]
      }
      
  • Scope Hoisting (依赖采用ES6模块化语法的代码)

    可以让Webpack 打包出来的代码更小。

    原理:分析模块之间的依赖关系,尽可能将被打散的模块合并到一个函数中,但前提是不能造成代码冗余。因此只有那些被引用了一次的模块才能被合并。

    使用如下:

    const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
    
    module.exports = {
      // ... 
     resolve: {
        // 针对Npm中的第三方模块优先采用jsnext:main中指向ES6模块化语法的文件
        mainFields:['jsnext:main', 'browser', 'main']
      },
      plugins: [
      // 开启Scope Hoisting
        new ModuleConcatenationPlugin()
       ],
       // ...
    

    注意:生产环境下默认开启。

最后

还有使用DllPluginDllReferencePlugin分离基础模块(vue-router、vuex等)、使用optimization.splitChunksoptimization.runtimeChunk分离公共代码。这两部分内容放到下篇总结。

参考文章:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章