Webpac优化看文记录

来自:https://mp.weixin.qq.com/s/UlIHZtis7T7ZODC1JmGUCQ

1.构建打点

要做优化,我们肯定得知道要从哪里做优化对吧。那在我们的一次构建流程中,是什么拉低了我们的构建效率呢?我们有什么方法可以将它们测量出来呢?

要解决这两个问题,我们需要用到一款工具:speed-measure-webpack-plugin,它能够测量出在你的构建过程中,每一个 Loader 和 Plugin 的执行时长,官方给出的效果图是下面这样:

而它的使用方法也同样简单,如下方示例代码所示,只需要在你导出 Webpack 配置时,为你的原始配置包一层 smp.wrap 就可以了,接下来执行构建,你就能在 console 面板看到如它 demo 所示的各类型的模块的执行时长。

const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

const smp = new SpeedMeasurePlugin();

module.exports = smp.wrap(YourWebpackConfig);

小贴士:由于 speed-measure-webpack-plugin 对于 webpack 的升级还不够完善,目前(就笔者书写本文的时候)还存在一个 BUG,就是无法与你自己编写的挂载在 html-webpack-plugin 提供的 hooks 上的自定义 Plugin (add-asset-html-webpack-plugin 就是此类)共存,因此,在你需要打点之前,如果存在这类 Plugin,请先移除,否则会产生如我这篇 issue 所提到的问题。

2.优化策略

主要有四大方向:缓存、多核、抽离、拆分,多核与拆分优化策略相信大家都不陌生,这里就不做过多讲解,这里我们主要详细讲解如何进行缓存与抽离。

2.1 缓存

我们每次的项目变更,肯定不会把所有文件都重写一遍,但是每次执行构建却会把所有的文件都重复编译一遍,这样的重复工作是否可以被缓存下来呢,就像浏览器加载资源一样?答案肯定是可以的,其实大部分 Loader 都提供了 cache 配置项,比如在 babel-loader 中,可以通过设置 cacheDirectory 来开启缓存,这样,babel-loader 就会将每次的编译结果写进硬盘文件(默认是在项目根目录下的node_modules/.cache/babel-loader目录内,当然你也可以自定义)。

但如果 loader 不支持缓存呢?我们也有方法。接下来介绍一款神器:cache-loader ,它所做的事情很简单,就是 babel-loader 开启 cache 后做的事情,将 loader 的编译结果写入硬盘缓存,再次构建如果文件没有发生变化则会直接拉取缓存。而使用它的方法很简单,正如官方 demo 所示,只需要把它卸载在代价高昂的 loader 的最前面即可:

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

小贴士cache-loader 默认将缓存存放的路径是项目根目录下的 .cache-loader 目录内,我们习惯将它配置到项目根目录下的 node_modules/.cache 目录下,与 babel-loader 等其他 Plugin 或者 Loader 缓存存放在一块

同理,同样对于构建流程造成效率瓶颈的代码压缩阶段,也可以通过缓存解决大部分问题,以 uglifyjs-webpack-plugin这款对于我们最常用的 Plugin 为例,它就提供了如下配置:

module.exports = {
  optimization: {
    minimizer: [
      new UglifyJsPlugin({
        cache: true,
        parallel: true,
      }),
    ],
  },
};

我们可以通过开启 cache 配置开启我们的缓存功能,也可以通过开启 parallel 开启多核编译功能,这也是我们下一章节马上就会讲到的知识。而另一款我们比较常用于压缩 CSS 的插件—— optimize-css-assets-webpack-plugin,目前我还未找到有对缓存和多核编译的相关支持,如果读者在这块领域有自己的沉淀,欢迎在评论区提出批正。

小贴士:目前而言笔者暂不建议将缓存逻辑集成到 CI 流程中,因为目前还仍会出现更新依赖后依旧命中缓存的情况,这显然是个 BUG,在开发机上我们可以手动删除缓存解决问题,但在编译机上过程就要麻烦的多。为了保证每次 CI 结果的纯净度,这里建议在 CI 过程中还是不要开启缓存功能。

2.2. 抽离

对于一些不常变更的静态依赖,比如我们项目中常见的 React 全家桶,亦或是用到的一些工具库,比如 lodash 等等,我们不希望这些依赖被集成进每一次构建逻辑中,因为它们真的太少时候会被变更了,所以每次的构建的输入输出都应该是相同的。因此,我们会设法将这些静态依赖从每一次的构建逻辑中抽离出去,以提升我们每次构建的构建效率。常见的方案有两种,一种是使用 webpack-dll-plugin 的方式,在首次构建时候就将这些静态依赖单独打包,后续只需要引用这个早就被打好的静态依赖包即可,有点类似“预编译”的概念;另一种,也是业内常见的 Externals的方式,我们将这些不需要打包的静态资源从构建逻辑中剔除出去,而使用 CDN 的方式,去引用它们。

 

2.2.1.webpack-dll-plugin 与 Externals 的抉择

团队早期的项目脚手架使用的是 webpack-dll-plugin 进行静态资源抽离,之所以这么做的原因是因为原先也是使用的 Externals,但是由于公司早期 CDN 服务并不成熟,项目使用了线上开源的 CDN 却因为服务不稳定导致了团队项目出现问题的情况,所以在一次迭代中统一替换成了 webpack-dll-plugin,但随着公司建立起了成熟的 CDN 服务后,团队的脚手架却因为各种原因迟迟没再更新。

而我,是坚定的 Externals 的支持着,这不是心之所向,先让我们来细数 webpack-dll-plugin 的三宗原罪:

  1. 需要配置在每次构建时都不参与编译的静态依赖,并在首次构建时为它们预编译出一份 JS 文件(后文将称其为 lib 文件),每次更新依赖需要手动进行维护,一旦增删依赖或者变更资源版本忘记更新,就会出现 Error 或者版本错误。

  2. 无法接入浏览器的新特性 script type="module",对于某些依赖库提供的原生 ES Modules 的引入方式(比如 vue 的新版引入方式)无法得到支持,没法更好地适配高版本浏览器提供的优良特性以实现更好地性能优化。

  3. 将所有资源预编译成一份文件,并将这份文件显式注入项目构建的 HTML 模板中,这样的做法,在 HTTP1 时代是被推崇的,因为那样能减少资源的请求数量,但在 HTTP2 时代如果拆成多个 CDN Link,就能够更充分地利用 HTTP2 的多路复用特性。口说无凭,直接上图验证结论:

  • 使用 webpack-dll-plugin 生成的 lib 文件,整体资源作为一个文件加载,需要 400 多毫秒

  •  使用 Externals 配合 HTTP2,所有资源并行加载,整体时长不超过 100ms

  • 这,就是我选择 Externals 的原因。

    但是,如果你的公司没有成熟的 CDN 服务,但又想对项目中的静态依赖进行抽离该怎么办呢?那笔者的建议还是选择webpack-dll-plugin 来优化你的构建效率。如果你还是觉得每次更新依赖都需要去维护一个 lib 文件特别麻烦,那我还是特别提醒你,在使用 Externals 时选择一个靠谱的 CDN 是一件特别重要的事,毕竟这些依赖比如 React 都是你网站的骨架,少了他们可是连站点都运行不起来了噢。

2.2.2.如何更为优雅地编写 Externals

我们都知道,在使用 Externals 的时候,还需要同时去更新 HTML 里面的 CDN,有时候时常会忘记这一过程而导致一些错误发生。那作为一名追求极致的前端,我们是否可以尝试利用现有资源将这一过程自动化呢?

这里我就给大家提供一个思路,我们先来回顾及分析一下,在我们配置 Externals 时,需要配置那些部分。

首先,在 webpack.config.js 配置文件内,我们需要添加 webpack 配置项:

module.exports = {
  ...,
  externals: {
    // key是我们 import 的包名,value 是CDN为我们提供的全局变量名
    // 所以最后 webpack 会把一个静态资源编译成:module.export.react = window.React
    "react": "React",
    "react-dom": "ReactDOM",
    "redux": "Redux",
    "react-router-dom": "ReactRouterDOM"
  }
}

与此同时,我们需要在模板 HTML 文件中同步更新我们的 CDN script 标签,一般一个常见的 CDN Link 就像这样:

https://cdn.bootcss.com/react/16.9.0/umd/react.production.min.js

这里以 BootCDN 提供的静态资源 CDN 为例(但不代表笔者推荐使用 BootCDN 提供的 CDN 服务,它上次更换域名的事件可真是让我踩了不少坑),我们可以发现,一份 CDN Link 其实主要也只是由四部分组成,它们分别是:CDN 服务 host、包名、版本号以及包路径,其他 CDN 服务也是同理。以上面的 Link 为例,这四部分对应的内容就是:

  • CDN 服务 host:cdn.bootcss.com/

  • 包名:react

  • 版本号:16.9.0

  • 包路径:umd/react.production.min.js

到了这一步,大家应该想到了吧。我们完全可以自己编写一个 webpack 插件去自动生成 CDN Link script 标签并挂载在 html-webpack-plugin 提供的事件钩子上以实现自动注入 HTML,而我们所需要的一个 CDN Link 的四部分内容,CDN 服务 host 我们只需要与公司提供的服务统一即可,包名我们可以通过 compiler.options.externals 拿到,而版本号我们只需要读取项目的 package.json 文件即可,最后的包路径,一般都是一个固定的值。

具体代码实现我就不作详细介绍了,团队在项目脚手架更新迭代期间,笔者已经根据公司提供的 CDN 服务定制了一款 Webpack 插件,实现逻辑就如上述所示,所以后续工程师们就不再需要去关注同步 script 标签了,一切都被集成进 Plugin 逻辑自动化处理了,当然,大家如果对插件的源码有兴趣,可以在评论区提出噢~笔者会考虑作为团队的开源项目贡献给社区。

3. 提升体验

3.1. webpack-build-notifier

这是一款在你构建完成时,能够像微信、Lark这样的APP弹出消息的方式,提示你构建已经完成了。也就是说,当你启动构建时,就可以隐藏控制台面板,专心去做其他事情啦,到“点”了自然会来叫你,同时还有提示音噢~

3.2. webpack-dashboard

当然,如果你对 webpack 原始的构建输出不满意的话,也可以使用这样一款 Plugin 来优化你的输出界面

4. 总结

综上所述,其实本质上,我们对与webpack构建效率的优化措施也就两个大方向:缓存和多核。缓存是为了让二次构建时,不需要再去做重复的工作;而多核,更是充分利用了硬件本身的优势(我相信现如今大家的电脑肯定都是双核以上了吧,我自己这台公司发的低配 MAC 都有双核),让我们的复杂工作都能充分利用我们的 CPU。而将这两个方向化为实践的主角,就是:cache-loader 和 happypack,所以你只要知道它并用好它,那你就能做到更好的构建优化实践。所以,别光看看,快拿着你的项目动手实践下,让你优化后的团队项目在你的 leader 面前眼前一亮吧!

原文地址:https://www.cnblogs.com/sxdjy/p/11796595.html