提升构建效率
webpack 构建流程
理解 Compiler 和 Compilation
- compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
- compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时使用。
Compiler的基本流程
具体源代码在 Compiler.js 中。
- readRecords:读取构建记录,用于分包缓存优化。
- compile 的主要构建过程,涉及以下几个环节:
- newCompilationParams:创建 NormalModule 和 ContextModule 的工厂实例,用于创建后续模块实例。
- newCompilation:创建编译过程 Compilation 实例,传入上一步的两个工厂实例作为参数
- compiler.hooks.make.callAsync:触发 make 的 Hook,执行所有监听 make 的插件
- compilation.finish:编译过程实例的 finish 方法,触发相应的 Hook 并报告构建模块的错误和警告
- compilation.seal:编译过程的 seal 方法,在后面会进一步分析。
- emitAssets:调用 compilation.getAssets(),将产物内容写入输出文件中。
emitRecords:对应第一步的 readRecords,用于写入构建记录。
Complation 的基本流程
这部分的源码位于 Compilation.js 中。其中,在编译执行过程中,我们主要从外部调用的是两个方法:
addEntry:从 entry 开始递归添加和构建模块。
seal:冻结模块,进行一系列优化,以及触发各优化阶段的 Hooks
总结
创建编译器 Compiler 实例。
- 根据 Webpack 参数加载参数中的插件,以及程序内置插件。
- 执行编译流程:创建编译过程 Compilation 实例,从入口递归添加和构建模块,模块构建完成后冻结模块,并进行优化。
- 构建与优化过程结束后提交产物,将产物内容写到输出文件中。
webpack 生命周期
通过webpack对外暴露的生命周期Hooks,理解整体流程的阶段划分
Webpack 工作流程中最核心的两个模块:Compiler 和 Compilation 都扩展自 Tapable 类,用于实现工作流程中的生命周期划分,以便在不同的生命周期节点上注册和调用插件。其中所暴露出来的生命周期节点称为Hook(俗称钩子)。
Webpack 中的插件
一个 Webpack 插件是一个包含 apply 方法的 JavaScript 对象。这个 apply 方法的执行逻辑,通常是注册 Webpack 工作流程中某一生命周期 Hook,并添加对应 Hook 中该插件的实际处理函数。例如下面的代码:
Hook 的使用方式
- 在构造函数中定义 Hooks 类型和参数,生成 Hook 对象。
- 在插件中注册 Hook,添加对应 Hook 触发时的执行函数。
- 生成插件实例,运行 apply 方法。
- 在运行到对应生命周期节点时调用 Hook,执行注册过的插件的回调函数。
正是通过这种方式,webpack 将编译器和编译过程的生命周期节点提供给外部插件,从而搭建起弹性化的工作引擎。
Compiler Hooks
构建器实例的生命周期可以分为 3 个阶段:初始化阶段、构建过程阶段、产物生成阶段。
- 初始化阶段
- environment、afterEnvironment:在创建完compiler实例且执行了配置内定义的插件的apply方法后触发。
- entryOption、afterPlugins、afterResolvers:在WebpackOptionsApply.js中,这3个Hooks分别在执行EntryOptions插件和其它内置插件,以及解析了resolver配置后触发。
- 构建过程阶段
- normalModuleFactory、contextModuleFactory:在两类工厂实例创建后触发。
- beforeRun、run、watchRun、beforeCompile、compile、thisCompilation、compilation、make、afterCompile:在运行构建过程中触发。
产物生成阶段
构建阶段
- addEntry、failedEntry:在添加入口和添加入口结束时触发。
- buildModule、failedModule、succeedModule:在构建单个模块时触发。
- finishModules:在所有模块构建完成后触发
- 优化阶段
- 在 seal 函数中共有 12 个主要的处理过程
编译阶段提速
在 Compiler 和 Compilation 的工作流程里,最耗时的阶段分别是哪个?
- 对于 Compiler 实例而言,耗时最⻓的显然是生成编译过程实例后的 make 阶段,在这个阶段里,会执行模块编译到优化的完整过程。而对于 Compilation 实例的工作流程来说,不同的项目和配置各有不同,但总体而言,编译模块和后续优化阶段的生成产物并压缩代码的过程都是比较耗时的。
- 不同项目的构建,在整个流程的前期初始化阶段与最后的产物生成阶段的构建时间区别不大。真正影响整个构建效率的还是 Compilation 实例的处理过程,这一过程又可分为两个阶段:编译模块和优化处理。
提升构建效率的三个方向:
- 使用 IgnorePlugin 忽略模块。如 moment 在构建时会自动引入 locale 下的多国语言包,一般情况下不需要,可以在构建时剔除,从而提升构建模块的速度和减少产物体积。
moment 比较大,意味着模块数量多,可以用更轻量的 dayjs 替换
按需引入类库模块。如我们常用的 lodash 库,在项目中只使用到了 lodash 的几个方法,但是构建时却发现引入了整个依赖包。解决方法是在导入声明的时候只导入特定的模块,这样就可以减少构建时间以及产物的体积。
Webpack 支持 Tree Shaking,又叫摇树优化,它能删除没有用到的代码,这可以减少产物包的体积。
- 要求导入的依赖包是使用 es6 module,而 lodash 是基于 commonjs,需要替换为 lodash-es 才能生效。
- 其相应的操作是在优化阶段进行,并不能减少模块编译阶段的构建时间。
预编译模块资源,是另一类减少构建模块的方式,它的核心思想是将项目依赖的框架等模块单独构建打包,与普通构建流程区分开。
- 思路:将 react, react-dom, redux, react-redux 基础包和业务基础包打包成一个文件,可以提供给其它项目使用。
- 方法:使用 DLLPlugin 进行分包,DllReferencePlugin 对 manifest.json 引用。
- 首先定义一个 webpack.dll.js,用于将基础库进行分离
- 然后再 webpack.common.js 中将预编译资源模块引入
- externals。webpack 配置中的 externals 和预编译模块资源解决的是同一类问题:将依赖的模块从构建过程中移除。它们的区别在于:
- 在 webpack 的配置方面,externals 更简单,而预编译模块资源需要独立的配置文件。
- 预编译模块资源需要独立构建,而 externals 直接使用 CDN 的地址。
- externals 配置的依赖包需要单独指定依赖模块的加载方式:全局对象、CommonJS、AMD 等。
提升单个模块构建的速度
提升编译阶段效率的第二个方向,是在保持构建模块数量不变的情况下,提升单个模块构建的速度。具体来说,是通过减少构建单个模块时的一些处理逻辑来提升速度。
的速度
- include/exclude。webpack 加载器配置中的 include/exclude,是常用的优化特定模块构建速度的方式之一。通过 include/exclude 排除的模块,并非不进行编译,而是使用 webpack 默认的 js 模块编译器进行编译。
- noParse。在上述 include/exclude 的基础上,进一步省略了使用默认 js 模块编译器进行编译的时间。
- Typescript 编译优化。在使用 ts-loader 编译 ts 时,会在编译前进行类型检查,这导致了编译时间比较⻓,通过加上配置项 transpileOnly: true,可以再编译时忽略类型检查,从而提升 ts 模块的编译速度;如果不想关闭,可以使用 ForkTsCheckerWebpackPlugin 插件,它会将检查过程移到单独的进程中。
- resolve。使用 resolve 配置减少文件搜索范围。
并行构建和swc以提升总体效率
并行构建
- thread-loader
- HappyPack(作者已不维护,推荐 thread-loader)
- 插件内置的 parallel 参数(如:TerserWebpackPlugin, CssMinimizerWebpackPlugin)
- Parallel-webpack
- swc。swc 是一个基于rust的可扩展平台,用于下一代快速开发工具;它被用于编译和打包,在单核算力下,它的编译速度要比 babel 快 20x,多核算力下编译速度则要快70x。
打包阶段提速
webpack 构建流程中的第二个阶段,也就是从代码优化到生成产物阶段的效率提升问题。
优化阶段效率提升的整体分析。webpack 的完整构建流程中,我们了解到整个优化阶段可以细分为 12 个子任务,每个任务依次对数据进行一定的处理,并将结果传递给下一个任务,所以这一阶段的优化可以分为两个不同的方向:
- 针对某些任务,使用效率更高的工具或配置项,从而提升当前任务的工作效率。
- 提升特定任务的优化效果,以减少传递给下一任务的数据量,从而提升后续环节的工作效率。
以提升当前任务工作效率为目标的方案
一般在项目的优化阶段,主要耗时的任务有两个:一个是生成 ChunkAssets,即根据 Chunk 信息生成 Chunk 的产物代码;另一个是优化 Assets,即压缩 Chunk 产物代码。
- 第一个任务主要在 Webpack 引擎内部的模块中处理,相对而言优化手段较少,主要集中在利用缓存方面。
- 而在压缩 Chunk 产物代码的过程中会用到一些第三方插件。一般在项目的优化阶段,主要耗时的任务有两个:一个是生成 ChunkAssets,即根据 Chunk 信息生成 Chunk 的产物代码;另一个是优化 Assets,即压缩 Chunk 产物代码。
持久化缓存
- 得益于 webpack5 中的持久化缓存,我们可以实现增量构建
- buildDependencies。它的作用是当配置文件内容或配置文件依赖的模块文件发生变化时,当前的构建缓存即失效持久化缓存
压缩
- terser-webpack-plugin
- css-minimizer-webpack-plugin
- html-minimizer-webpack-plugin
- esbuild
以提升后续环节工作效率为目标的方案
- 分包。是指在 Chunk 生成之后,将原先以入口点来划分的 Chunks 根据一定的规则,分离出子 Chunk 的过程;它有许多优点,如有利于缓存命中、能提升后续环节的工作效率(通过分包来抽离多个入口点引用的公共依赖)。优化阶段的另一类优化方向是通过对本环节的处理减少后续环节处理内容,以便提升后续环节的工作效率。
- Tree Shaking 摇树优化。在构建打包过程中,移除那些引入但未被使用的无效代码。
设置为 chunks: ‘all’,则能够将所有的重复依赖情况都进行分包处理,从而减少了重复引入相同模块代码的情况。hunks: ‘all’,则能够将所有的重复依赖情况都进行分包处理,从而减少了重复引入相同模块代码的情况。