webpack 构建优化实践

434 阅读5分钟

内容导读:

序言:

webpack对于前端的同学总是熟悉又陌生,熟悉的是几乎每个框架都有 webpack 身影,而且你也可以根据自己的需求独自搭建一个适合自己的框架结构,说到最后,其实都为了一个目的,快和简单,为了每次编译快一点,为了每次打包快一点,以及能让设计的系统更适合自己的所期望的理想架构,记得之前看 react 文档是作者说的一句话:不要为了用而用,再恰当时机使用才能得到最好的效果。由于之前也接触过 webpack ,也总结了到一些关于 webpack 的如何加快构建速度的方法,所以想在已有项目上尝试一下。

用到的技术方法:

本次实践主要根据 webpack 文档构建性能章节来进行逐步实现的。如使用的到的主要包括:

  • Dllplugin : 使用 DllPlugin 可以将不频繁改动的代码单独编译,拆分了 bundle 同时降低构建的复杂度。要使用 DllPlugin 就需要在一个单独的 webpack 中 创建一个 dll-only-bundle,然后生成一个 manifest.json 的文件,此文件这个文件包含了从 requireimportrequest 到模块 id 的映射。 此插件与 output.library 的选项相结合可以暴露出(也称为放入全局作用域) dll 函数。另一方面让 DllReferencePlugin 能够映射到相应的依赖上。

    为什么要使用 DllPlugin ?在项目开发过程中,我们的代码包含第三方库代码和业务代码,而我们主要开发的是业务代码,第三方库代码除了升级的情况一般不改动,所以此时将第三方包通过 DllPlugin 打包成动态链接库来引用

    注:webpack5 新出一个联邦模块可以支持跨项目共享 bundles,实现热插拔效果,感兴趣的同学可以了解一下,个人感觉是 DllPluginplus 版。

    对应的依赖关系图
  • Thread-loader : 是一个多进程打包,即它会将每一个在他之后loader的放进一个单独的 woker 池(一个单独的 node 进程)里面,由于每一个 worker 池都是一个单独的 node 进程,所以各进程间不能进行通信,每次打开一个进程也是有消耗的,所以建议在消耗大的 loader 上使用。

如何在项目中应用

DllPlugin 在项目中的应用

  1. 创建 vue.vendor.config.js 文件

    const path = require('path')
    const webpack = require('webpack')
    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    
    module.exports = {
      mode: 'production',
      entry: {
        vendor: [
          'vue',
          'vuex',
          'vue-router',
          'element-ui',
          'axios',
          'lodash',
          'echarts',
          'codemirror']
      },
      // 输出的配置
      output: {
        path: path.join(__dirname, 'public/vendor'),
        filename: '[name].dll.js',
        library: '[name]_[hash]'
      },
      plugins: [
        new CleanWebpackPlugin(), // 再次打包需要清除之前已生成的文件
        new webpack.DllPlugin({ // 生成 dll.js 文件和 manifest.json 文件
          path: path.join(__dirname, 'public/vendor', '[name]-manifest.json'), // 定义文件的输出路径
          name: '[name]_[hash]',
          context: process.cwd()
        })
      ]
    }
    
  2. 在本地的 vue.config.js 文件中配置 DllReferencePlugin 用于在构建过程中寻找依赖。

  • DllReferencePlugin 用于查找映射依赖
  • AddAssetHtmlPlugin 用于自动将生成 dll.js 文件作为 script 的引用插入到 html 中。
module.exports = {
  configureWebpack: {
    plugins: [
      new webpack.DllReferencePlugin({
        manifest: require('./public/vendor/vendor-manifest.json')
      }),
      new AddAssetHtmlPlugin({
        filepath: path.resolve(__dirname, './public/vendor/*.js'),
        publicPath: './vendor',
        outputPath: './vendor'
      })
    ]
  }
}
  1. package.json 文件中添加生成 dll.js 的执行命令
{
  "scripts": {
    "dll": "webpack --progress --config ./vue.vendor.config.js"
  }
}
  1. 执行命令 yarn dll 后会在 public/vendor 下生成一个 vendor-manifest.json 文件和一个 vendor.dll.js ,此时就可以正常开始启动项目进行正常开发了。

    • 生成 manifest.json 文件的部分内容如下:

      {
        "name": "vendor_99b4edce0862b7b404e3",
        "content": {
          "./node_modules/zrender/lib/core/util.js": {
            "id": 0,
            "buildMeta": {
              "providedExports": true
            }
          },
          "./node_modules/echarts/lib/echarts.js": {
            "id": 1,
            "buildMeta": {
              "providedExports": true
            }
          },
          "./node_modules/echarts/lib/util/graphic.js": {
            "id": 2,
            "buildMeta": {
              "providedExports": true
            }
          }
        }
      }
      ```~
      

    thread-loader 应用

    如何防止 thread-loader 启动 worker 带来的高延时?

    thread-laoder 由于每启动一个 node 进程都会有一定的时间开销,所以其提供的有预热功能,可以防止启动 worker 带来的高延时。在本项目中开启使用 thread-loader 开启多进程的有 babel-loaderts-loade

    1. vue.config.js 中配置

      const threadLoader = require('thread-loader')
      
      const tsPoolOptions = { // thread-loader 参数
        name: 'ts-pool',
        workers: 2, // 启动 worker 的数量 默认是cpu核心数减1
        poolRespawn: process.env.NODE_ENV === 'production',
        workerParallelJobs: 50,
        poolParallelJobs: 50,
        poolTimeout: 2000
      }
      
      threadLoader.warmup(tsPoolOptions, // 预热
        ['babel-loader'] // 加载模块
      )
      
      module.exports = {
        chainWebpack(config) {
          config
            .when(process.env.NODE_ENV !== 'development',
              config => {
      
                config.module
                  .rule('ts')
                  .use('thread-loader')
                  .before('babel-loader')
                  .loader('thread-loader')
                  .options(tsPoolOptions)
                  .end()
              }
            )
        }
      }
      

使用前后对比

build 时间对比

  • 更改前

  • 更改后

build 后包大小对比

  • 更改前

  • 更改后

使用后感受

DllPlugin

  • 首先使用上可能有些麻烦,需要将不需要使用的第三方资源包先进行编译才可以在build时生效,每次打包只打包业务代码,但是在更改第三包资源时需要再次进行 dll 编译。dll 时会把整个第三方引用包都会被编译,所以如果只是使用某个的包的部分功能,可以使用按需引用,避免 dll 编译后的 bundle 过大。

thread-loader

  • 只能处理耗时的 loader
  • 使用开销比较大,每开启一个 worker 进程池需要 600ms

本次实践感想

本次操作实践在一定程度上让自己对 webpack 打包和编译有了更多了解,在其中学习到的很多内容,也对自己的知识进行了巩固,明白的知道和做到之间其实是有很大的区别的,在开发的整个过程可以了解到优化的真实效果,以及是否适合自己所用的项目。其实在大家的客观上目标是一致的,只不过主观上有所区别,但是殊途同归。