前端起dev从110秒减少到7秒, 开发体验大幅提升

[webpack由浅入深]系列的内容

  • 第一层: 了解一个小功能的完整流程. 看完可以满足好奇心应付原理级别面试.
  • 第二层: 源码陪读, webpack源码比较灵活, 自己看容易陷入迷惑. 文章里会贴出关键流程的代码来辅助阅读源码. 如果你正在自己调试, 在这些方法上下断点会节约你宝贵的时间.

webpack cache 发布3年多了, 在历史包袱中的项目中其实非常好用.

本文会介绍 cache 在一个项目中的实践经验, 和实现流程, 以及了解流程后的一些推论.

webpack cache 实践经验

我的实践经验是基于公司的一个 monorepo 老项目.

效果是单个子项目的dev速度从110秒减少到了7秒, 单个文件改动10秒.

下面说的经验也都是基于这个例子.

合适的使用场景

webpack cache 的效果是用磁盘空间换 compile 速度.

所以在我看来, webpack cache 更合适在本地 dev 的场景使用, 因为本地 dev 触发 compile 比 ci 服务器频繁得多, 并且改动更小, 可以命中更多缓存, 也能大幅提升开发体验.

实践

在实践中, 缓存的命中率没什么可操作性. 优化空间都在减少占用磁盘空间上. 在我的项目中, 我做了以下配置:

  1. 如果配置 cache 的文件是读取配置文件的, 要将buildDependencies 配置为你的文件, 而不是 __filename. 在我们公司打包脚本中是一个 webpack-chain 文件.
  2. monorepo 子包会有一些公共依赖, 在 module resolve 的时候也会指到主包的 node_modules, 在这种情况下 cache 配置的 cacheDirectory 可以让多个子包指到同一个文件夹, 来节省cache空间. (在 dev 的时候 react-refresh 会产生大几百m的缓存, 是起码可以节省的)
  3. 合理设置 maxAge, 超过 maxAge 的未被使用的缓存会被清除.

我的看法是生产设置小, dev看自己电脑空间, 如果足够的话可以不设置. (默认一个月)

webpack cache 实现流程

下面会深入一下 cache 的实现流程, 了解流程除了满足好奇心, 还可以:

  • 根据特殊场景优化配置.

  • 了解什么边缘情况会造成缓存占用磁盘大.

  • 根据自己需求二开 cache.

webpack cache 的实现流程职能分层非常清晰, 并且只有一个分层比较复杂, 其他都很简单.

我们从 compile 时调用 cache 说起.

在 compile 流程中读取与保存 cache

webpack流程相关的前置知识如果不清楚, 需要先看以往的文章来补一下, 再继续回这里.

compilation里有三个变量: _modulesCache, _assetsCache, _codeGenerationCache. 分别在对应的时间点读取和写入 cache:

  • _modulesCache 读取: 在 addModule 的时候读取. module 的 build 在读取之后, 如果命中 cache, 那么needBuild就会是false, 跳过这个 module 的 build, 来节省时间. (build 做的事是运行 loader 和 parse 并分析 ast )

  • _modulesCache 写入: 在 module 的 build 完成之后, 把 build 后的 module 结果按照 module 的 id 存储起来.

  • _codeGenerationCache: 读取和写入分别在module.codeGeneration()的前后.

  • _assetsCache: 在最终生成 assets 的阶段, 在获取 manifest 以后读取 cache, 如果命中, 则不逐个调用fileManifest.render()来产生 assets 了. 如果不命中, 则调用 render 后写入 cache.

(这里调用的时候加了层包装是因为这里的 cache 都要匹配 hash )

这些 cache 的来源和相关的调用时机

在代码中可以看到, 这些 cache 都是调用 compiler.getCache() 获得的, 也就是 compiler.cache()封装了一层 facade.

this.cache就是new Cache(). 所以上面章节的 cache, 都是 new Cache()实例的调用.

另外可以看到, this.cache在 compiler 中, 还在对应的流程中调用了 beginIdle, endIdle, shutdown, 和storeBuildDependencies.

buildDependency 不影响功能先不看, 其他的调用之后展开.

通过 option 指向不同的 cache 实现

进入到Cache类里, 发现所有方法的时间都是调用了 tapable.

cache 的具体实现, 是在 apply option 的时候注入的. (文件是 WebpackOptionsApply, 方法是 process )

在这里可以看到, case 很少, 只有2个.

第一个是使用内存, 第二个是使用文件系统写入硬盘.

内存使用里的MemoryCachePlugin非常简单:

在内存里建立一个map, 分别在外部调用get(), 和store()方法的时候调用对应的map的方法.

另外在shutdown的时候把map清了.

对, 就是这么简单. 其实文件系统也这么简单, 复杂的点是读取和写入硬盘.

写入文件的 cache 实现: IdleFileCachePlugin

现在我们来看写入硬盘的实现: IdleFileCachePlugin.

先看getstore方法, 其实就是调用了strategy.storestrategy.restore. 只是多写几行代码来保证所有的写操作都做完再读.

除此之外, 还在 beginIdle 和 shutdown 的时候调用了strategy.afterAllStored来持久化 cache.

PackFileCacheStrategy 主要功能

进入到strategy, 我们关注store, restore, 和afterAllStored方法.

先看store, 和restore方法, 通过_getPack()获取到从硬盘读取的结构化数据pack, 分别调用packget()方法和set()方法.

afterAllStored的作用是把数据持久化到硬盘. 第一步也是获取内存里的pack数据, 再经过一定处理来写到硬盘中.

经过观察可以看到, _openPack()的读取硬盘, 和afterAllStored()的写入文件, 都是通过fileSerializer()来进行的.

cache 在内存, 与文件系统的最大区别, 其实就在于持久化的过程, 对于 cache 的读取和写入都是差不多的.

而下面要说的fileSerializer做的事, 就把内存中的格式化数据向硬盘读写, 并且尽量优化减少写入的体积.

整理数据与写入和读取文件的 Serializer

这一节是最复杂的, 主要研究对象是fileSerializer的2个方法serialize()deserialize().

并且优化逻辑是和上面提到的pack和相关实体的数据结构紧密相关的.

首先看fileSerializer以 middleware 的形式来分代码职责, 执行fileSerializerserialize()或deserialize()的时候, 会轮流执行各个 middleware 的对应的serialize()或deserialize()方法.

构造时候的 middleware 有:

  1. SingleItemMiddleware: 转化数组/单个元素的, 我感觉就没啥用, 没体会到意义.
  2. ObjectMiddleware: 在序列化的时候, 调用目标数据自己的函数, 进行数据整理.
  3. binaryMiddleware: 序列化/反序列化成二进制.
  4. fileMiddleware: 读取/写入硬盘.

下面展开讲一下我关注的ObjectMiddleware.

ObjectMiddlewarepack的读取/写入优化

先来看ObjectMiddlewareserialize()deserialize()方法.

他们的模式其实是一样的: 构造一个上下文ctx来给序列化/反序列化的数据对应的方法调用.

其实 s/ds 的直接目标都是PackContainer对象, 所以会在 s/ds 的过程中调用PackContainer的 s/ds 方法.

ctx中提供的write, read方法可以操作正在被ObjectMiddleware处理的数据, 从而影响ObjectMiddleware的处理结果.

另外可以看到PackContainerwriteLazy的目标是this.data, 也就是pack对象, 并且write()会触发pack对象的 s/ds 方法.

经过debug, PackContainer里的内容其实是差不多的, 所以核心内容就是pack的 s/ds 方法了.

pack 的数据结构与优化

这是最后一部分, 但比较复杂, 我只有能力简单的说一下.

首先说几个 pack 的关键属性:

  • content: 他是真正存放内容的地方. 但奇怪的他不是一个 map, 而是一个数组.

用意是数组的每个元素最后会被写成单独的文件, 通过一些优化, 每次改动可以只写有改动的 cache 所对应的文件

  • itemInfo: 这个是保存数据关系的地方.

他的键是 id, packget(), set() 的第一步都是先从itemInfo中通过 id 找到对应的信息.

他的值是对应的信息, 信息内容有: etag 对比 hash; location 存储信息在 content 数组的哪个位置; lastAccess 每次 get 会更新值, 在垃圾回收的时候配合 maxAge 决定是否清理; freshValue 如果不存储在 content 中, 他是一个刚被建立的内容, 值就存在这里, 相对的, location 有值的时候这里是没值的.

  • invalid: 如果pack完全没动, 这个变量可以快速判断. 第一次 set() 操作就会把他置为 true.

如果熟悉了这些属性, 那么packset()get()方法就非常好理解了.

最后, packserialize()的方法中进行了垃圾回收的操作,

就结果而言, 就是合理地对pack的数据结构进行一些更新. (主要就是 content 和 itemInfo, lazy, outdated 判断和变更)

但其过程在我能力范围之外, 以后能力有提升的话再回来分析.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/12370.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java 语言的“编译与解释并存”

Java 语言被称为“编译与解释并存”,是因为它结合了编译型语言和解释型语言的特点,具有独特的运行机制。这个特点是由 Java 的编译器和 Java 虚拟机 (JVM) 共同实现的。下面我们详细剖析这一过程,并通过具体示例进行说明。 编译与解释的过程…

2024护网蓝队面试题

2024护网蓝队面试题 一. 目前有防火墙,全流量检测,态势感知,IDS,waf,web服务器等设备,如何搭建一个安全的内网环境,请给出大概拓扑结构 (适用于中高级) 1.1 全流量与态…

根据ip限制接口访问次数

前言 我们利用redis去实现这个功能,redis的天然高并发和内存单线程速度拉满,非常适合做这个场景。为了可用性,我们把它封装成注解形式,哪个接口想被根据ip限制接口访问次数,直接标注上注解即可。 一、添加配置 在yaml…

mysql的隔离性——MVCC

MVCC通过undolog版本链和readview来实现 更新和删除时会写入undolog中。 读已提交:在事务任意读时创建readview,读最新提交的事务 可重复读:在事务第一次读时创建readview

【opencv】图像畸变校正

接上篇文章:【鱼眼+普通相机】相机标定 附代码: 方法一: 使用cv2.undistort """Create May 11, 2024author Wang Jiajun """import cv2 import numpy as npdef correct(img,camera_fileE:/cali…

使用Caché管理工具

Cach通过一个web工具来对其进行系统管理和完成管理任务,该方法的一个好处是不必将Cach安装到用于管理的系统上。目前,通过网络远程管理和控制对站点的访问,这些都比较容易。因为数据及其格式信息都直接来自被管理的系统,因此,这也可以最小化跨版本的兼容问题。 本文将描述…

lua面向对象

建议提前学习https://www.runoob.com/lua/lua-metatables.html 面向对象特征 1) 封装:指能够把一个实体的信息、功能、响应都装入一个单独的对象中的特性。2) 继承:继承的方法允许在不改动原程序的基础上对其进行扩充&#xff0…

图的深度优先遍历

way:栈,map(或set,只是我想用map)记录是否访问过,放入时记录为已访问,打印,邻接的没访问过先入cur,再入邻接的节点,放入一个邻接的节点后及时break去下一个深…

Kubernetes二进制(单master)部署

文章目录 Kubernetes二进制(单master)部署一、常见的K8S部署方式1. Minikube2. Kubeadmin3. 二进制安装部署4. 小结 二、K8S单(Master)节点二进制部署1. 环境准备1.1 服务器配置1.2 关闭防火墙1.3 修改主机名1.4 关闭swap1.5 在/e…

(done) 关于 pytorch 代码里常出现的 batch_first 到底是啥?

参考文章:https://pytorch.org/docs/stable/generated/torch.nn.utils.rnn.pad_sequence.html 首先看参考文章里的解释,如下图 从文章描述来看,当 batch_first True 时,输出的张量的 size 是 B x T x *。当 batch_first False…

umi搭建react项目

UMI 是一个基于 React 的可扩展企业级前端应用框架,提供路由、状态管理、构建和部署等功能,可以帮助开发者快速构建复杂的单页面应用(SPA)和多页面应用(MPA)。它与 React 的关系是,UMI 构建在 R…

0.0和0.00竟然不相等!!!BigDecimal别用错了比较方式

对于BigDecimal字段,可以使用compareTo()方法和equals()方法进行比较。但是要注意这两种方法的作用有所不同。一般都应该使用BigDecimal比较值,而不是使用经常用到的equals方法比较内容。 1.compareTo()方法 是用来比较两个BigDecimal对象的大小关系。…

出现dependencies.dependency.version‘ for xxxx:jar is missing的解决方法

目录 1. 问题所示2. 原理分析3. 解决方法1. 问题所示 出现如下问题:dependencies.dependency.version for xxxx:jar is missing. 且一直提示Pom文件缺失依赖包(由于公司项目,此处不放图) 2. 原理分析 这个错误通常发生在 Maven 项目中,表示在项目的依赖关系中找不到指定…

大数据知识点分享:Python的固定语法

Python编码声明 为源文件指定特定的字符编码,需要在py文件的首行或第二行插入一行特殊的注释行 #-*-coding:utf-8-*- 2.单行注释 单行注释以井号(#)开头 # 这是一个单独成行的注释 print(Hello, World!) # 这是一个在代码后面的注释 3…

移动端自动化测试工具 Appium 之 main 启动

文章目录 一、背景二、生成xml文件2.1、创建xml方法2.2、执行主类MainTest2.3、自动生成的xml2.4、工程目录2.5、执行结果 三、命令行执行appium服务四、主方法启动类五、集成Jenkins六、总结 一、背景 Jenkins 做集成测试是不错的工具,那么UI自动化是否可以&#…

图解自动驾驶中的运动规划(Motion Planning),附几十种规划算法

目录 1 自动驾驶驶向何处?2 什么是运动规划?3 运动规划实战教程4 加入我们5 订阅需知 1 自动驾驶驶向何处? 自动驾驶,又称无人驾驶,是依靠计算机与人工智能技术在没有人为操纵的情况下,完成完整、安全、有效…

2.1.2 事件驱动reactor的原理与实现

LINUX 精通 2 day14 20240513 day15 20240514 算法刷题:2维前缀和,一二维差分 耗时 135min 习题课 4h 课程补20240425 耗时:4h 课程链接地址 回顾 怎么学0voice课网络io——一请求一线程,一个client一个连接再accpet分配io f…

linux系统修改网卡名称

说明: 因操作过程需要停用网卡,导致ssh远程连接不上,需要控制台登录操作。 测试环境: CentOS7.9、8.2虚拟机 Suse15 SP4虚拟机 操作步骤: 方法一: 1、 查看网卡当前名称及状态 ip a2、 将网卡状态从启用…

记一次苹果appstore提审拒审问题1.2

有关苹果appstore审核1.2问题的处理方案 2023.8.6苹果回复 Bug Fix Submissions The issues weve identified below are eligible to be resolved on your next update. If this submission includes bug fixes and youd like to have it approved at this time, reply to thi…

Flutter 中的 CupertinoActionSheet 小部件:全面指南

Flutter 中的 CupertinoActionSheet 小部件:全面指南 在Flutter中,CupertinoActionSheet是用于在iOS风格的应用中显示动作面板的组件。它提供了一个简洁的界面,让用户可以快速从一组选项中做出选择。CupertinoActionSheet通常伴随着一个或多…