不知不觉Webpack原理系列已经发了十篇,逐步推进到插件、Loader、模块、runtimes、Chunk、依赖对象,从key的含义和运行原理模块依赖图等概念,到HMR、Tree-Shaking等特性的功能介绍和原理分析,一共十篇,总共5W多字,基本实现了Webpack的整个核心流程。接下来我会继续沿着Webpack冷门的方向,推出两个比较实用的系列:基础应用和性能优化。性能优化系列主要介绍在Webpack场景下如何通过配置、插件等方式优化构建和运行性能,以及这些性能优化背后的核心原理,比如Webpack5新的缓存功能将在本文介绍。经过这么多年使用持久缓存的发展,Webpack生态已经形成了非常全面和强大的前端工程能力,但庞大而全面的运行性能背后却逐渐被业界所诟病。类似的框架在业界引起了不小的轰动。为此,Webpack终于在第五个大版本中引入了持久化缓存,以提升运行性能。持久缓存是Webpack5最令人兴奋的特性之一,它可以将第一次构建的结果持久化到本地文件系统,在下次构建时跳过解析、链接、编译等一系列非常耗性能的操作被执行,直接复用模块和块的构建结果。使用持久化缓存后,构建性能有了很大的提升!以Three.js为例,该项目包含362个JS文件,总共约30000行代码,算是一个中大型项目:配置babel-loader和eslint-loader后,在Tested在我的机器上,当不使用缓存功能时,构建时间大约需要11000ms到18000ms;启用缓存功能后,第二次构建时间缩短到500ms到800ms之间,两者相差接近“50”倍!而这将近50倍的性能提升,只需要在Webpack5场景下设置cache.type='filesystem'即可启用:module.exports={//...cache:{type:'filesystem'},//...};原理那么,为什么开启持久化缓存后构建性能提升如此之大呢?总之,Webpack5会将第一次构建的Module、Chunk、ModuleGraph等对象序列化保存到硬盘中。运行时,可以跳过一些耗时的编译动作,直接复用缓存信息。构建流程在《Webpack 原理系列》中,我们已经讨论了很多关于Webpack构建功能的运行流程和实现细节。为了加深对缓存的理解,有必要从构建性能的角度简单回顾一下。Webpack的构建过程大致可以分为三个阶段:初始化,主要是根据配置信息设置各种内置插件Make-构建阶段,从入口模块开始,执行:读入文件内容并调用Loader翻译文件内容调用acorn生成AST结构分析AST,确定模块依赖列表遍历模块依赖列表,对每个依赖的模块重新执行上述过程,直到生成完整的模块依赖图——ModuleGraphobjectSeal-生成阶段,过程:代码翻译,比如import转换为require调用分析运行时依赖遍历模块依赖图,对每个模块执行:Merge模块代码和运行时代码,生成chunk进行产品优化操作,比如Tree-shaking在将最终结果写入产品文件的过程中有很多CPU密集型的操作,such由于调用Loader链加载文件,在遇到babel-loader、eslint-loader、ts-loader等工具时可能需要重复生成AST;分析模块依赖信息时,需要遍历AST,进行大量计算;Seal阶段同样有很多AST走,还有代码转换,优化操作等等。实现缓存在引入持久化缓存之前,Webpack每次运行都需要对所有模块完成上述构建过程。假设业务项目中有1000个文件,每次执行npxwebpack命令需要从0开始执行1000次构建,生成逻辑。但是Webpack5的持久化缓存功能会尝试将构建结果保存到文件系统中,在下次编译时比较每个文件的内容哈希或时间戳,对没有变化的文件跳过编译操作,直接使用缓存的副本,减少重复计算;发生变化的模块将重新执行编译过程。缓存执行时序如下图所示:如图所示,第一次构建后,Webpack将Module、Chunk、ModuleGraph这三类对象的状态序列化记录到缓存文件中;在下一次构建开始时,尝试读入并恢复这些对象的状态,从而跳过执行Loader链、解析AST、解析依赖等耗时操作,提高编译性能。详细使用了解了缓存的核心原理后,我们再回过头来看看缓存提供的配置项列表。下面介绍几个比较常用的配置项:官方文档:https://webpack.js.org/configuration/cachecache.type:缓存类型,支持'memory'|‘filesystem’,需要设置文件系统开启持久化缓存cache.cacheDirectory:缓存文件存放路径,默认为node_modules/.cache/webpackcache.buildDependencies:附加依赖文件当这些文件内容发生变化时,缓存将彻底失效并会进行一次完整的编译构建,通常可以设置为项目配置文件,如:module.exports={cache:{buildDependencies:{config:[path.join(__dirname,'webpack.dll_config.js')],},},};cache.managedPaths:托管目录,Webpack构建时会跳过新旧代码哈希值和时间戳的比较,直接使用缓存的副本。默认值为['。/node_modules']cache.profile:是否输出缓存处理过程的详细日志,默认为falsecache.maxAge:缓存过期时间,默认值为5184000000使用时,平时注意上面的配置即可items,还有idleTimeout、idleTimeoutAfterLargeChanges等其他item。和Webpack内部实现算法有关,和缓存效果关系不大,可以不用关注。Webpack4中的缓存其实Webpack4内置了一个使用内存实现的临时缓存功能,但是必须在watch模式下使用,进程退出后会立即失效,不太实用。但是在Webpack4及之前的版本中,可以使用一些loader内置的缓存功能来提升构建性能,比如babel-loader、eslint-loader、cache-loader。开启babel-loader缓存,只需设置cacheDirectory=true开启babel-loader持久缓存功能,例如:module.exports={//...module:{rules:[{test:/\.m?js$/,loader:'babel-loader',options:{cacheDirectory:true,},}]},//...};配置项说明:https://github.com/babel/babel-loader#options以Three.js为例,开启缓存后,生产环境构建时间从3500ms减少到1600ms;开发环境构建时间从6400ms减少到4500ms,性能提升约30%到50%。默认情况下,babel-loader会将缓存的内容保存到node_modules/.cache/babel-loader目录下,用户也可以通过cacheDirectory='dir'设置缓存路径。开启eslint-loader缓存eslint-loader也支持缓存功能,设置cache=true开启即可,如:module.exports={//...module:{rules:[{test:/\.js$/,exclude:/node_modules/,loader:'eslint-loader',options:{cache:true,},},]},//...};配置项说明:https://github.com/webpack-contrib/eslint-loader#cache仍然以Three.js为例。启用缓存后,生产环境构建时间从6400ms减少到1400ms;开发环境搭建时间从7000ms减少到2100ms,性能提升70%到80%。默认情况下,babel-loader会将缓存的内容保存到./node_modules/.cache/eslint-loader目录下,用户也可以通过cache='dir'设置缓存路径。使用cache-loader除了babel-loader、eslint-loader等专门加载器承载的缓存功能外,还可以使用Webpack4中的cache-loader实现类似Webpack5的通用持久化缓存功能,只需要:使用它cache-loader配置在loader数组的顶部,例如:constpath=require("path");constwebpack=require("webpack");module.exports={//...module:{规则:[{test:/\.js$/,use:['cache-loader','babel-loader','eslint-loader']}]},//...};缓存加载器文档:https://www.npmjs。com/package/cache-loader使用cache-loader后,生产环境构建时间从10602ms减少到1540ms;开发环境构建时间从11130ms减少到4247ms,性能提升约“60%~80%”。与Webpack5自带的持久化缓存不同,cache-loader只对Loader执行结果有效,缓存范围和深度不如内置的缓存功能,所以性能收益比较低,但它已经是Webpack4版本下性能优化的一种简单有效的方法。总结网上关于Webpack持久缓存的讨论很少,但这确实是Webpack5引入的一个特别令人兴奋的特性,在某些情况下甚至可以让构建性能达到Unbundle解决方案的水平。相信在Webpack5中,持久化缓存将成为Webpack性能优化的利器。本文转载自微信公众号「Tecvan」
