webpack 4 源码主流程分析(十四):webpack 优化

1,969 阅读7分钟

原文首发于 blog.flqin.com。如有错误,请联系笔者。分析码字不易,转载请表明出处,谢谢!

前面一至十一章,介绍了在 development 的模式下,整个完整了构建主流程。在了解构建流程的基础上,本章整理一些与 webpack 优化相关的知识点。

production 模式

我们参考 production 模式里,里面已经做了大部分的优化,如压缩,Scope Hoistingtree-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.shouldEmitcompilation.hooks.shouldRecord
    • 功能:如果在 compilation 编译时有 error,则不执行 Record 相关的钩子,并且抛错和不编译资源
  • OccurrenceOrderModuleIdsPluginOccurrenceOrderChunkIdsPlugin
    • 注意不是文档写的 OccurrenceOrderPlugin,这个没用
    • 触发时机:compilation.hooks.optimizeModuleOrdercompilation.hooks.optimizeChunkOrder
    • 功能:根据模块初始调用次数或者总调用次数排序(配置),这样在后面分配 ID 的时候常被调用 ID 就靠前,除此之外,还可以让 id 为路径,hash 等。
    • 对应配置项:optimization.occurrenceOrderoptimization.chunkIdsoptimization.moduleIds
  • SideEffectsFlagPlugin
    • 触发时机:normalModuleFactory.hooks.modulecompilation.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.hashForChunkcompilation.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 都为最新
  • 对于 jscssimg 等其他静态资源:开启缓存,将静态资源上传到 cdn,对资源开启长期缓存,因为有唯一 hash 的缘故所以不会导致资源被覆盖,用户在初次访问可以将这些长效缓存下载到本地,然后在后续的访问可以直接从缓存里读,节约网络资源。

webpack 中的持久化缓存

  • js 使用 chunkhash ,对 css 应用 mini-css-extract-plugin 插件并使用 contenthash
  • 通过 optimization.moduleIds 属性设置 module id
    • 开发环境 moduleIds 设为 named 即使用 NamedModulesPlugin (相对路径为 key)来固化 module id
    • 生产环境 moduleIds 设为 hashed 即使用 HashedModuleIdsPlugin (将路径转换为 hashkey)来固化 module id,保证在某一模块增删后,不会影响其他模块的 module id
  • 通过 optimization.chunkIds 属性设置为 namedoptimization.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 模块语法(即 importexport),目的是为了供程序静态分析
  • 确保没有 compilerES2015 模块语法转换为 CommonJS 模块(设置 babel.config.js presets: [['@babel/env', { modules: false }]]
  • package.json 或者 module.rule 设置 sideEffects : false,告诉 webpack 该项目或者该文件没有副作用
  • mode 选项设置为 production,其中会启用 FlagDependencyUsagePluginTerserPlugin 完成 tree shaking

Scope Hoisting

Scope Hoisting 即 作用域提升,可以让 webpack 打包出来的代码文件更小,运行更快。

Scope Hoisting 优点

  • 代码体积会变小,因为函数声明语句会产生大量代码
  • 代码在运行时因为创建的函数作用域减少了,内存开销也随之变小

Scope Hoisting 原理

ES6 的静态模块分析,分析出模块之间的依赖关系,按照引用顺序尽可能地把模块放到同一个函数作用域中,然后适当的重命名一些变量以防止变量名冲突。

异步 import() 不会启用 Scope Hoisting

启用 Scope Hoisting 需要满足

  • 使用 ES2015 模块语法(即 importexport
  • mode 选项设置为 production,其中会启用 ModuleConcatenationPlugin 插件完成 Scope Hoisting

一些插件

以下列举部分我用过优化相关的插件及 loader

  1. happypack 多线程编译,加快编译速度 注:已被废弃,使用 thread-loader
  2. webpackbar 编译进度条
  3. mini-css-extract-plugin 提取 css 样式到单独文件
  4. style-ext-html-webpack-plugin 增强 HtmlWebpackPlugin,将 css 内联到 html
  5. script-ext-html-webpack-plugin 增强 HtmlWebpackPlugin,将 js 内联到 html
  6. optimize-css-assets-webpack-plugin 使用cssnano压缩优化 css
  7. webpack-bundle-analyzer 模块分析
  8. url-loader 将文件转换为 DataURL,减少请求数
  9. speed-measure-webpack-plugin 构建耗时分析

各插件随着时间推移,有的可能废弃,有的可能被更好的所替代,已社区流行为准。

一些其他优化点

  1. 缓存二次构建,如 babel-loaderterser-webpack-plugin 开启缓存,使用 cache-loader,使用 hard-source-webpack-plugin(已被 webpack5 内置) 等
  2. 分包构建,如 DLLPlugin+DllRefrencePlugin
  3. 缩小构建范围,如 module.rulesinclude/exclude,配置 resolve.modules/resolve.mainFields/resolve.extensions ,配置 noParse,配置 externals, 配置 IgnorePlugin

后记

webpack 源码开始,到后面打包结果分析、watchwebpack 优化总结等,前前后后花了一个月的时间,但收获也颇多。由于对 webpack 主流程的执行有了大概的认知,在遇到一些配置需要深入了解专研的时候,能快速定位在流程的哪个环节;在开发一个 loader 或者 plugin 也能有很清晰的思路;最重要的是通过对源码分析,大型工程的组织架构,扩展性,健壮性等给人带来一些新的思路和启发。

本系列到此结束,后续会不断的更新优化。对 webpack 的主流程分析解除了我心中很多的构建相关的疑惑,解开了心中的结。人生短短数十载,精力、时间都很有限,选择做让自己开发的事情,方为上策。

webpack5.0 已到,后续有时间会分析与 webpack 4.x 不同的源码差异。

如有错误,请联系笔者。分析码字不易,转载请表明出处,谢谢!