源码分析(十四):webpack 优化
前面一至十一章,介绍了在 development 的模式下,整个完整了构建主流程。在了解构建流程的基础上,本章整理一些与 webpack 优化相关的知识点。
production 模式
我们参考 production 模式里,里面已经做了大部分的优化,如压缩,Scope Hoisting, tree-shaking 等都给予了我们启发,接下来具体分析各个点。
production 模式启用的插件
FlagDependencyUsagePlugin- 触发时机:
compilation.hooks.optimizeDependencies - 功能:标记模块导出中被使用的导出,存在
module.usedExports里。用于Tree shaking。 - 对应配置项:
optimization.usedExports:true
- 触发时机:
FlagIncludedChunksPlugin- 触发时机:
compilation.hooks.optimizeChunkId - 功能:给每个
chunk添加了ids,用于判断避免加载不必要的chunk
- 触发时机:
ModuleConcatenationPlugin- 触发时机:
compilation.hooks.optimizeChunkModules - 功能:使用
esm语法可以作用域提升(Scope Hoisting)或预编译所有模块到一个闭包中,提升代码在浏览器中的执行速度 - 对应配置项:
optimization.concatenateModules:true
- 触发时机:
NoEmitOnErrorsPlugin- 触发时机:
compiler.hooks.shouldEmit,compilation.hooks.shouldRecord - 功能:如果在
compilation编译时有error,则不执行Record相关的钩子,并且抛错和不编译资源
- 触发时机:
OccurrenceOrderModuleIdsPlugin,OccurrenceOrderChunkIdsPluginSideEffectsFlagPlugin- 触发时机:
normalModuleFactory.hooks.module,compilation.hooks.optimizeDependencies - 功能:
normalModuleFactory.hooks.module钩子里读取package.json里的sideEffects字段和读取module.rule里的sideEffects赋给module.factoryMeta(纯的ES2015模块);compilation.hooks.optimizeDependencies钩子里根据sideEffects配置,删除未用到的export导出
- 对应配置项:
optimization.sideEffects:true(默认)
- 触发时机:
TerserPlugin- 触发时机:
template.hooks.hashForChunk,compilation.hooks.optimizeChunkAssets - 功能:
- 在
template.hooks.hashForChunk钩子即在chunks生成hash阶段会把压缩相关的信息也打入到里面 - 在
compilation.hooks.optimizeChunkAssets钩子触发资源压缩事件
- 在
- 对应配置项:
optimization.minimize是否开启压缩optimization.minimizer定制Terser,默认开启多进程压缩和缓存
- 触发时机:
另:development 模式单独启用的插件:
NamedChunksPlugin- 触发时机:
compilation.hooks.beforeChunkIds - 功能:以名称固化
chunk id - 对应配置项:
optimization.chunkIds
- 触发时机:
NamedModulesPlugin- 触发时机:
compilation.hooks.beforeModuleIds - 功能:以名称固化
module id - 对应配置项:
optimization.moduleIds
- 触发时机:
持久化缓存
在更新部署页面资源时,无论是先部署页面,还是先部署其他静态资源,都会因为新老资源替换后的缓存原因,或者部署间隔原因,都会导致资源不对应而引起页面错误。
持久化缓存方案就是在各静态资源的名字后面加唯一的 hash 值,这样在每次修改文件后生成的不同的 hash 值,然后在增量式发布文件时,就可以避免覆盖掉之前旧的文件。获取到新文件的用户就可以访问新的资源,而浏览器有缓存等情况的用户则继续访问老资源,保证新老资源同时存在且互不影响不出错。
- 对于
html:不开启缓存,把html放到单独的服务器上并关闭服务器的缓存,需要保证每次的html都为最新 - 对于
js,css,img等其他静态资源:开启缓存,将静态资源上传到cdn,对资源开启长期缓存,因为有唯一hash的缘故所以不会导致资源被覆盖,用户在初次访问可以将这些长效缓存下载到本地,然后在后续的访问可以直接从缓存里读,节约网络资源。
webpack 中的持久化缓存
- 对
js使用chunkhash,对css应用mini-css-extract-plugin插件并使用contenthash - 通过
optimization.moduleIds属性设置module id- 开发环境
moduleIds设为named即使用NamedModulesPlugin(相对路径为key)来固化module id, - 生产环境
moduleIds设为hashed即使用HashedModuleIdsPlugin(将路径转换为hash为key)来固化module id,保证在某一模块增删后,不会影响其他模块的module id
- 开发环境
- 通过
optimization.chunkIds属性设置为named或optimization.namedChunks属性设置为true(通过将chunk name复制到chunk id)固化chunk id,该属性会启用NamedChunksPluginNamedChunksPlugin插件里可以自定义nameResolver设置namesplitChunks.cacheGroups[].name也可以设置chunk name- 魔法注释也可以设置:
import(/* webpackChunkName: "my-chunk-name" */ 'module')
- 通过
optimization.splitChunks属性抽离库vendor,业务公共代码common - 通过
optimization.runtimeChunk属性抽离运行时runtime,其中runtime也可以通过script-ext-html-webpack-plugin插件嵌入到html
Tree Sharing
Tree Sharing 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。由 rollup 普及,在 webpack 里由 TerserPlugin 实现。
tree-sharing 原理
ES6的模块引入是静态分析的,故而可以在编译时正确判断到底加载了什么代码- 分析程序流,判断哪些变量未被使用、引用,进而删除此代码
如果我们引入的模块被标记为 sideEffects: false,只要它任意一个导出都没有被其他模块引用到,那么不管它是否真的有副作用,整个模块都会被完整的移除。
"
side effect(副作用)" 的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个export或多个export。举例说明,例如polyfill,它影响全局作用域,并且通常不提供export。
启用 tree shaking 需要满足
- 使用
ES2015模块语法(即import和export),目的是为了供程序静态分析 - 确保没有
compiler将ES2015模块语法转换为CommonJS模块(设置babel.config.jspresets: [['@babel/env', { modules: false }]]) - 在
package.json或者module.rule设置sideEffects : false,告诉webpack该项目或者该文件没有副作用 - mode 选项设置为
production,其中会启用FlagDependencyUsagePlugin,TerserPlugin完成tree shaking
Scope Hoisting
Scope Hoisting 即 作用域提升,可以让 webpack 打包出来的代码文件更小,运行更快。
Scope Hoisting 优点
- 代码体积会变小,因为函数声明语句会产生大量代码
- 代码在运行时因为创建的函数作用域减少了,内存开销也随之变小
Scope Hoisting 原理
ES6 的静态模块分析,分析出模块之间的依赖关系,按照引用顺序尽可能地把模块放到同一个函数作用域中,然后适当的重命名一些变量以防止变量名冲突。
异步
import()不会启用Scope Hoisting
启用 Scope Hoisting 需要满足
- 使用
ES2015模块语法(即import和export) mode选项设置为production,其中会启用ModuleConcatenationPlugin插件完成Scope Hoisting
一些插件
以下列举部分我用过优化相关的插件及 loader:
- happypack 多线程编译,加快编译速度 注:已被废弃,使用 thread-loader
- webpackbar 编译进度条
- mini-css-extract-plugin 提取
css样式到单独文件 - style-ext-html-webpack-plugin 增强
HtmlWebpackPlugin,将css内联到html里 - script-ext-html-webpack-plugin 增强
HtmlWebpackPlugin,将js内联到html里 - optimize-css-assets-webpack-plugin 使用cssnano压缩优化
css - webpack-bundle-analyzer 模块分析
- url-loader 将文件转换为
DataURL,减少请求数 - speed-measure-webpack-plugin 构建耗时分析
各插件随着时间推移,有的可能废弃,有的可能被更好的所替代,已社区流行为准。
一些其他优化点
- 缓存二次构建,如
babel-loader,terser-webpack-plugin开启缓存,使用cache-loader,使用hard-source-webpack-plugin(已被webpack5内置) 等 - 分包构建,如
DLLPlugin+DllRefrencePlugin等 - 缩小构建范围,如
module.rules里include/exclude,配置resolve.modules/resolve.mainFields/resolve.extensions,配置noParse,配置externals, 配置IgnorePlugin等
后记
从 webpack 源码开始,到后面打包结果分析、watch、webpack 优化总结等,前前后后花了一个月的时间,但收获也颇多。由于对 webpack 主流程的执行有了大概的认知,在遇到一些配置需要深入了解专研的时候,能快速定位在流程的哪个环节;在开发一个 loader 或者 plugin 也能有很清晰的思路;最重要的是通过对源码分析,大型工程的组织架构,扩展性,健壮性等给人带来一些新的思路和启发。
本系列到此结束,后续会不断的更新优化。对 webpack 的主流程分析解除了我心中很多的构建相关的疑惑,解开了心中的结。人生短短数十载,精力、时间都很有限,选择做让自己开发的事情,方为上策。
webpack5.0 已到,后续有时间会分析与 webpack 4.x 不同的源码差异。
如有错误,请联系笔者。分析码字不易,转载请表明出处,谢谢!