webpack 构建优化策略

802 阅读6分钟

 ~  ~前言:相信绝大部分前端工程师对webpack基本配置已经是烂熟于心了,所以webpack系列的第一篇,我们基于构建优化应用打包开始,然后一点一点补充完成整个webpack体系。
 ~  ~ 而使用webpack打包躲不开的就是webpack优化这个话题,无论是面试还是实际开发,优化都是非常重要的事情,毕竟提升用户体验是我们前端工程师的职责

基于构建策略优化应用打包

本篇将从3个大方向进行webpack优化,分别是:减少打包体积优化构建时间优化用户体验


减少打包体积

splitChunks

最初,chunks(以及内部导入的模块)是通过内部 webpack 图谱中的父子关系关联的。CommonsChunkPlugin 曾被用来避免他们之间的重复依赖,但是不可能再做进一步的优化。

开箱即用的 SplitChunksPlugin 对于大部分用户来说非常友好。

默认情况下,它只会影响到按需加载的 chunks,因为修改 initial chunks 会影响到项目的 HTML 文件中的脚本标签。

webpack 将根据以下条件自动拆分 chunks:

  • 新的 chunk 可以被共享,或者模块来自于 node_modules 文件夹
  • 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积)
  • 当按需加载 chunks 时,并行请求的最大数量小于或等于 30
  • 当加载初始化页面时,并发请求的最大数量小于或等于 30

当尝试满足最后两个条件时,最好使用较大的 chunks。

Tree Shaking

tree shaking - 摇树优化 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的静态结构特性,例如import 和export。这个术语和概念实际上是由 ES2015 模块打包工具rollup 普及起来的。

webpack中只需将打包环境设置为生产环境就能让摇树优化生效,同时业务代码使用ESM编写,使用import导入模块,使用export导出模块。

按需加载

将路由页面/触发性功能单独打包为一个文件,使用时才加载,好处是减轻首屏渲染的负担。因为项目功能越多其打包体积越大,导致首屏渲染速度越慢。

首屏渲染时只需首页的JS代码而无需其他页面的JS代码,所以可用按需加载实现。webpack v4+提供模块按需切割加载功能,配合import()可做到首屏渲染减包的效果,以加快首屏渲染速度。只有当触发某些功能时才会加载当前功能的JS代码

作用提升

分析模块间依赖关系,把打包好的模块合并到一个函数中,好处是减少函数声明与内存花销作用提升首次出现于rollup,是rollup的核心概念,后来在webpack v3中借鉴过来使用。

在未开启作用提升前,构建后的代码会存在大量函数闭包。因为模块依赖,通过webpack打包后会转换为IIFE,大量函数闭包包裹代码会导致打包体积增大,模块越多越明显。在运行代码时创建的函数作用域变多,导致更大的内存开销。

在开启作用提升后,构建后的代码会根据引入顺序放到一个函数作用域中,通过适当重命名某些变量以防止变量名冲突,以减少函数声明与内存花销。

webpack中只需将打包环境设置为生产环境就能让作用提升生效,或显式设置concatenateModules

压缩资源

压缩HTML/CSS/JS代码,压缩字体/图像/音频/视频,好处是更有效减少打包体积。极致地优化代码都有可能不及优化一个资源文件的体积更有效。

针对CSS/JS代码,分别使用以下插件开启压缩功能。其中OptimizeCss基于cssnano封装,UglifyjsTerser都是webpack官方插件,同时需注意压缩JS代码需区分ES5ES6


优化构建时间

thread-loader

配置Thread将Loader单进程转换为多进程,好处是释放CPU多核并发的优势。在使用webpack构建项目时会有大量文件需解析与处理,构建过程是计算密集型的操作,随着文件增多会使构建过程变得越慢。

Node中运行的webpack是单线程模型。简而言之,就是webpack待处理的任务需一件件处理,不能同一时刻处理多件任务。

文件读写计算操作无法避免,能不能让webpack同一时刻处理多个任务,发挥多核CPU电脑的威力以提升构建速度呢?thread-loader来帮你,根据CPU个数开启线程。

在 worker 池中运行的 loader 是受到限制的。例如:

  • 这些 loader 不能生成新的文件。
  • 这些 loader 不能使用自定义的 loader API(也就是说,不能通过插件来自定义)。
  • 这些 loader 无法获取 webpack 的配置。

每个 worker 都是一个独立的 node.js 进程,其开销大约为 600ms 左右。同时会限制跨进程的数据交换。

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /.js$/,
        include: path.resolve('src'),
        use: [
          "thread-loader",
          // 耗时的 loader (例如 babel-loader)
        ],
      },
    ],
  },
};
// with options
use: [
  {
    loader: "thread-loader",
    // 有同样配置的 loader 会共享一个 worker 池
    options: {
      // 产生的 worker 的数量,默认是 (cpu 核心数 - 1),或者,
      // 在 require('os').cpus() 是 undefined 时回退至 1
      workers: 2,

      // 一个 worker 进程中并行执行工作的数量
      // 默认为 20
      workerParallelJobs: 50,

      // 额外的 node.js 参数
      workerNodeArgs: ['--max-old-space-size=1024'],

      // 允许重新生成一个僵死的 work 池
      // 这个过程会降低整体编译速度
      // 并且开发环境应该设置为 false
      poolRespawn: false,

      // 闲置时定时删除 worker 进程
      // 默认为 500(ms)
      // 可以设置为无穷大,这样在监视模式(--watch)下可以保持 worker 持续存在
      poolTimeout: 2000,

      // 池分配给 worker 的工作数量
      // 默认为 200
      // 降低这个数值会降低总体的效率,但是会提升工作分布更均一
      poolParallelJobs: 50,

      // 池的名称
      // 可以修改名称来创建其余选项都一样的池
      name: "my-pool"
    },
  },
  // 耗时的 loader(例如 babel-loader)
];

缓存副本

配置cache缓存Loader对文件的编译副本,好处是再次编译时只编译修改过的文件。未修改过的文件干嘛要随着修改过的文件重新编译呢?

大部分Loader/Plugin都会提供一个可用编译缓存的选项,通常包括cache字眼。以babel-loadereslint-webpack-plugin为例。

export default {
  // ...
  module: {
    rules: [{
      // ...
      test: /\.js$/,
      use: [{
        loader: "babel-loader",
        options: { cacheDirectory: true }
      }]
    }]
  },
  plugins: [
    new EslintPlugin({ cache: true })
  ]
};

优化用户体验

模块懒加载

如果不进行模块懒加载的话,最后整个项目代码都会被打包到一个js文件里,单个js文件体积非常大,那么当用户网页请求的时候,首屏加载时间会比较长,使用模块懒加载之后,大js文件会分成多个小js文件,网页加载时会按需加载,大大提升首屏加载速度

合理配置hash

我们要保证,改过的文件需要更新hash值,而没改过的文件依然保持原本的hash值,这样才能保证在上线后,浏览器访问时没有改变的文件会命中缓存,从而达到性能优化的目的

// webpack.base.js

output: {
  path: path.resolve(__dirname, '../dist'),
  // 给js文件加上 contenthash
  filename: 'js/chunk-[contenthash].js',
  clean: true,
},

小结

最后,让我们一起加油吧!

gg.jpg

都看到这了,不如顺手点个赞再走 ( *ˇωˇ* )