Webpack 构建速度的优化

作者: 贺鹏飞 分类: 工程化 发布时间: 2021-01-25 16:16

一、使用高版本的 Webpack 和 Node.js

1、webpack 5.x 主要带来了哪些新特性:

更快的构建速度

缓慢的构建一直是开发人员对Webpack抱怨点之一。现在,模块打包提供了一个可选的文件系统缓存。通过设置合适的缓存系统,我们可以大大加快构建速度,大大提高开发人员的工作效率。

更高的版本要求

nodejs的版本做了提升,抛弃了旧的版本,使用10.13以上版本,这让可以移除掉大量的历史遗留代码。添加了实验性的WebAssembly,Async Web Assembly,Await等特性,为日后的升级做了准备。

更灵活的模块组合

允许多个Webpack构建协同工作。webpack5允许不同的应用程序从不同版本的webpack动态加载代码。通过更灵活的模块组合,我们可以构建更灵活的前端架构。

更智能的缓存优化

通过缓存,访问应用程序的可以得到非常满意的加载体验。使用webpack 5,我们对局部模块代码的修改,不会影响其它模块的缓存,这样我们可以大大提高我们开发应用的体验。

更小的体积

新版本的webpack对代码进行了模块化的管理,可以检测到无用代码,可以删除未使用的代码,可以删除模块内部的代码,打包后的体积将会大幅度缩小。

2、Node.js 15.x 代来了那些新特性:

AbortController

AbortController接口表示一个控制器对象,允许开发者根据需要中止一个或多个 Web请求,Node.js 15 加入了 AbortController 的一个实验性实现。AbortController 是一个全局实用工具类,可根据 AbortController Web API在选定的基于 Promise 的 API 中取消发出的请求信号。

npm 7

Node.js 15 附带了 npm 的新大版本 npm 7。npm 7 有许多新功能,包括 npm 工作区和新的 package-lock.json 格式。npm 7 还包括 yarn.lock 文件支持。npm 7 的一大变化是默认安装对等依赖项。

unhandled rejections 默认抛出

以前unhandled rejections默认是warn,15版本改为throw,在 throw 模式下,如果未设置 unhandledRejection hook,则会将 unhandledRejection提升为未捕获的异常。

QUIC(实验)

QUIC 是谷歌制定的一种基于UDP的低时延的互联网传输层协议,它是 HTTP/3 的基础传输协议。
Node.js 15 附带 QUIC 的实验性支持,可通过 --experimental-quic配置标志编译 Node.js 来启用。
在2016年11月国际互联网工程任务组(IETF)召开了第一次QUIC工作组会议,受到了业界的广泛关注,意味着QUIC开始在成为新一代传输层协议上迈出了关键的一步。同时,QUIC 具有内置的 TLS 1.3 安全性、流控制、错误纠正、连接迁移和多路复用。

N-API 7

N-API是一个用于构建本机插件的C语言的API,它独立于底层JavaScript运行时环境(如V8),并作为Node.js本身的一部分。它是为了将Addons插件和底层JavaScript引擎的改动隔离开来,并且允许在一个版本编译的模块不需要重新编译就可以在更高版本的Node.js上运行,它确保了Node.js版本和不同编译器级别之间应用程序接口(ABI)的稳定性。

V8 8.6

V8 JavaScript 引擎已更新为 V8 8.6(V8 8.4 是 Node.js 14 中的最新版本)。除了性能调整和改进之外,V8 更新还带来了以下语言特性:

  • Promise.any()
    Promise.any() 接收一个Promise可迭代对象,只要其中的一个 promise 成功,就返回那个已经成功的 promise 。如果可迭代对象中没有一个 promise 成功(即所有的 promises 都失败/拒绝),就返回一个失败的 promise 和AggregateError类型的实例,它是 Error 的一个子类,用于把单一的错误集合在一起。
  • AggregateError
    AggregateError主要用于操作报告多个错误被抛出的场景。
  • String.prototype.replaceAll()
    replaceAll() 方法是返回一个新字符串,新字符串所有满足 pattern 的部分都已被replacement 替换。pattern可以是一个字符串或一个 RegExp, replacement可以是一个字符串或一个在每次匹配被调用的函数。

二、多进程/多实例构建:HappyPack(不维护了)、thread-loader

多进程打包:某个任务消耗时间较长会卡顿,多进程可以同一时间干多件事,效率更高。优点是提升打包速度,缺点是每个进程的开启和交流都会有开销(babel-loader消耗时间最久,所以使用thread-loader针对其进行优化)。

    {
      loader: 'thread-loader',
      options: {
        workers: 2 // 进程数量 2个
      }
    }

三、使用 webpackPrefetch、webpackPreload

Webpack 4.6.0为我们提供了预先拉取(prefetching)和预先加载(preloading)的功能。使用这些声明可以修改浏览器处理异步chunk的方式。webpackPrefetch 会在浏览器闲置下载文件,webpackPreload 会在父 chunk 加载时并行下载文件。

四、压缩代码

  • webpack-paralle-uglify-plugin;
  • uglifyjs-webpack-plugin 开启 parallel 参数 (不支持ES6);
  • terser-webpack-plugin 开启 parallel 参数;
  • 多进程并行压缩;
  • 通过 mini-css-extract-plugin 提取 Chunk 中的 CSS 代码到单独文件,通过 css-loader 的 minimize 选项开启 cssnano 压缩 CSS。

五、图片压缩

  • 使用基于 Node 库的 imagemin (很多定制选项、可以处理多种图片格式);
  • 配置 image-webpack-loader。

六、缩小打包作用域

  • exclude/include (确定 loader 规则范围) ;
  • resolve.modules 指明第三方模块的绝对路径 (减少不必要的查找);
  • resolve.mainFields 只采用 main 字段作为入口文件描述字段 (减少搜索步骤,需要考虑到所有运行时依赖的第三方模块的入口文件描述字段);
  • resolve.extensions 尽可能减少后缀尝试的可能性 ;
  • noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句);
  • IgnorePlugin (完全排除模块);
  • 合理使用alias。

七、提取页面公共资源

  • 使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中;
  • 使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件;
  • 基础包分离。

八、分包编译

  • 使用 DllPlugin 进行分包;
  • 使用DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间;
  • HashedModuleIdsPlugin 可以解决hash 频繁变动的问题。

九、充分利用缓存提升二次构建速度

  • babel-loader 开启缓存;
  • terser-webpack-plugin 开启缓存;
  • 使用 cache-loader 或者 hard-source-webpack-plugin。

十、Tree shaking 移除多余的代码

  • purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议);
  • 打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率;
  • 禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking;
  • 使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码。

十一、Scope hoisting

  • 构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突;
  • 必须是ES6的语法,因为有很多第三方库仍采用 CommonJS 语法,为了充分发挥 Scope hoisting 的作用,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法。

十二、动态Polyfill

建议采用 polyfill-service 只给用户返回需要的polyfill,社区维护。(部分国内奇葩浏览器UA可能无法识别,但可以降级返回所需全部polyfill)。

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注