源码分析(十四):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
,OccurrenceOrderChunkIdsPlugin
SideEffectsFlagPlugin
- 触发时机:
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
,该属性会启用NamedChunksPlugin
NamedChunksPlugin
插件里可以自定义nameResolver
设置name
splitChunks.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.js
presets: [['@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
不同的源码差异。
如有错误,请联系笔者。分析码字不易,转载请表明出处,谢谢!