- 前端
- 可观测性
- 开发工具
【免费下载链接】rrweb
record and replay the web
rrweb 以"录制即事件流"的方式工作,录制数据量与页面复杂度和用户交互频率成正比,在高交互场景下可能远超预期。本文基于官方文档 docs/recipes/optimize-storage.zh_CN.md(英文版见 docs/recipes/optimize-storage.md),系统梳理四类存储优化策略:屏蔽 DOM 元素、sampling 抽样降频、压缩与去冗,并结合 packages/rrweb 与 packages/packer 的源码实现,讲清每个配置项的真实生效机制与默认行为。读完本文,你将能够为你的录制场景组合出一套可落地的存储优化方案。
优化策略整体分为三类:
- 屏蔽 DOM 元素:减少录制的内容范围;
- sampling 配置抽样:减少录制的数据量与事件频率;
- 去冗与压缩:减少数据存储体积。
何时需要优化存储:识别"数据爆炸"场景
在默认配置下,rrweb 会录制全量快照、增量 DOM 变更、鼠标移动/交互、滚动、输入、媒体交互、视口变化等事件流。以下场景最容易产生高于预期的数据量:
- 长列表:滚动会持续触发大量滚动事件与 DOM 变更;
- 复杂的 SVG:节点数量大,序列化后体积可观;
- 包含 JS 控制动画的元素:每一帧都可能产生属性变更或子树变更的 mutation 事件;
- canvas 动画:逐帧绘制调用(或逐帧截图)本身就是高数据量来源。
这些元素往往不是回放时的关注重点,因此第一层优化就是让 rrweb 直接忽略它们。
策略一:屏蔽 DOM 元素,缩小录制范围
blockClass / blockSelector:整棵子树不入镜
在调用record时传入blockClass(默认值为'rr-block')或blockSelector,命中该选择器的元素会被视为"阻塞元素",其整个子树都不会进入录制数据,回放时以占位形式呈现。
import { record } from '@rrweb/record'; record({ emit(event) {}, blockClass: 'rr-block', // 默认值,页面中带 rr-block class 的子树不录制 blockSelector: '.ads, #live-region', // 额外用 CSS 选择器指定要屏蔽的元素 });从源码看,屏蔽判断发生在快照(snapshot)与增量(incremental)两条链路中。核心实现是 packages/rrweb-snapshot/src/snapshot.ts 中的_isBlockedElement:它支持blockClass为字符串(用classList.contains判断)或正则表达式(对每个 className 做test)。同时在 packages/rrweb/src/record/observer.ts 中,几乎所有事件观察器(鼠标交互、滚动、输入、媒体交互等)在派发事件前都会调用isBlocked做前置过滤,例如initMouseInteractionObserver中对目标元素执行isBlocked(target, blockClass, blockSelector, true)(见 observer.ts)。因此被屏蔽的元素既不会出现在快照中,也不会产生增量事件。
ignoreClass / ignoreSelector:保留结构、忽略内容
与block不同,ignore(默认 class 为'rr-ignore')表示元素结构仍然会被录制,但元素内部的变化(属性、子树变更)被忽略。典型用途是聊天消息列表、日志面板等高频刷新区域:结构占位保留,内容变化不产生事件。
需要权衡的副作用
屏蔽会带来一个直接后果:被屏蔽区域在回放中不可见。如果该区域是产品核心 UI(例如价格、图表),屏蔽策略会导致回放信息缺失。因此建议只对与业务无关或非关注点的高数据量区域使用,并配合下面的抽样与压缩策略分层使用。
策略二:sampling 抽样:从"频率"维度削减事件
当录制内容无法减少时,可以通过sampling配置降低各类事件的录制频率甚至完全关闭。SamplingStrategy的完整定义位于 packages/types/src/index.ts,下面结合源码逐一说明每个字段的行为与默认值。
示例 1:整体开关与节流阈值
import { record } from '@rrweb/record'; record({ emit(event) {}, sampling: { // 不录制鼠标移动事件 mousemove: false // 不录制鼠标交互事件 mouseInteraction: false, // 设置滚动事件的触发频率 scroll: 150 // 每 150ms 最多触发一次 // set the interval of media interaction event media: 800 // 设置输入事件的录制时机 input: 'last' // 连续输入时,只录制最终值 } })各字段的真实生效机制(均可在 packages/rrweb/src/record/observer.ts 中找到对应实现):
| 字段 | 取值 | 默认行为 | 源码实现 |
|---|---|---|---|
mousemove | false或毫秒数 | 默认节流阈值 50ms | initMoveObserver(observer.ts)中sampling.mousemove === false直接跳过注册;为数字时作为位置采集的节流阈值(L118-L119) |
mousemoveCallback | 毫秒数 | 默认 500ms | 控制"位置批量上报回调"的节流间隔(L120-L123),与mousemove分层节流 |
mouseInteraction | false、true或对象 | 默认录制全部交互 | initMouseInteractionObserver(observer.ts)中=== false时跳过注册(L200) |
scroll | 毫秒数 | 默认 100ms | initScrollObserver中throttle(..., sampling.scroll \|\| 100)(L348) |
media | 毫秒数 | 默认 500ms | initMediaInteractionObserver中throttle(..., sampling.media \|\| 500)(L1048) |
input | 'all'|'last' | 默认'all'(监听input与change) | sampling.input === 'last'时只监听change事件,配合cbWithDedup对重复值去重(L466-L484) |
几个容易忽视的细节:
mousemove: false与mousemoveWait的关系:mousemoveWait是旧版参数。在 packages/rrweb/src/record/index.ts 中,当mousemoveWait存在而sampling.mousemove未定义时,会自动迁移到sampling.mousemove,两者等价,建议统一使用sampling。input: 'last'的意义:连续输入多字符时只记录最终值,牺牲"逐键回放"的精细度,换取输入事件数量的大幅下降。如果你的回放需要精确还原输入过程,请保留'all'。- 阈值型参数的取舍:
scroll、media等节流阈值越大,事件越稀疏,回放时滚动/媒体进度会呈现"跳跃感",建议在数据量与回放平滑度之间取平衡。
示例 2:细粒度开关鼠标交互类型
当页面中某类交互事件量极大(例如持续聚焦/失焦触发Focus/Blur),可以只关闭特定类型:
import { record } from '@rrweb/record'; record({ emit(event) {}, sampling: { // 定义不录制的鼠标交互事件类型,可以细粒度的开启或关闭对应交互录制 mouseInteraction: { MouseUp: false, MouseDown: false, Click: false, ContextMenu: false, DblClick: false, Focus: false, Blur: false, TouchStart: false, TouchEnd: false, }, }, });这里的键名与 packages/types/src/index.ts 中MouseInteractions枚举一一对应(还包括TouchMove_Departed、TouchCancel等)。源码中initMouseInteractionObserver会将对象形式的配置转换为disableMap,只有disableMap[eventKey] !== true时才注册对应监听器(observer.ts),从而实现"按需开关"。
其他抽样项:canvas
SamplingStrategy还包含canvas字段:'all'表示逐条录制所有 canvas 绘制调用;设置为 1~60 之间的数字时,将以该频率(每秒最多次数)在 Web Worker 中录制 canvas 截图快照(仅支持OffscreenCanvas的环境可用)。对于 canvas 动画场景,这是比"整元素屏蔽"更精细的折中方案。
策略三:压缩:从"体积"维度削减存储
基于 packFn 的单事件压缩
rrweb 在 packages/packer 中提供了一个基于fflate的简单压缩函数,可以直接作为packFn传入录制配置:
import { pack } from '@rrweb/packer'; record({ emit(event) {}, packFn: pack, });其底层实现见 packages/packer/src/pack.ts:先JSON.stringify事件,再用zlibSync(基于 fflate 的 deflate/zlib 实现)压缩,最后以二进制安全字符串输出。同时会给事件附加v: 'v1'标记(见 packages/packer/src/base.ts 中的MARK常量),用于回放时识别压缩版本。
从 packages/rrweb/src/record/index.ts 的eventProcessor可以看到,packFn在每个事件通过emit输出前被调用,也就是说每个 event 独立压缩。需要注意:当启用跨域 iframe 录制并向父窗口传递事件(passEmitsToParent)时,事件不会被packFn处理。
回放时,需要把packer.unpack作为unpackFn传入Replayer:
import { unpack } from '@rrweb/packer'; import { Replayer } from '@rrweb/replay'; const replayer = new Replayer(events, { unpackFn: unpack, });unpack的实现(packages/packer/src/unpack.ts)具备较强的健壮性:先尝试按普通 JSON 解析(兼容未压缩的历史数据),再尝试按压缩数据解压,并通过v === MARK校验版本兼容性,最后才抛出Unknown data format错误。这意味着你可以混合使用压缩与未压缩的数据,只要数据本身格式合法。
批量压缩:更推荐的服务端方案
基于packFn的单事件压缩以每个 event为单位进行,这往往不能发挥 rrweb 录制数据易于压缩的优势——相邻事件之间(尤其是连续的全量快照、样式表、长文本输入)存在大量重复片段,而单个小 JSON 的 zlib 压缩无法利用跨事件的冗余。
因此文档更加推荐在服务端实现多个 event 的批量压缩,例如将单次用户操作(一个 session / 一次请求上下文)产生的所有 event 合并为一个大块再压缩。对于 gzip、zlib 等压缩算法而言,更大的输入块意味着更高的压缩比,这在长会话场景下收益显著。
策略四:去冗:只存一份"相同的数据"
压缩之外,另一个优化思路是去冗(deduplication)。
为了在回放中准确模拟 hover 等效果,rrweb 会尽可能地将 CSS 样式inline进录制数据(即inlineStylesheet: true,这是 packages/rrweb/src/record/index.ts 的默认值)。可以想象,如果使用 rrweb 录制大量用户对同一站点的访问,每个 session 的录制数据中都会保存几乎完全相同的样式表内容——N 个用户就是 N 份拷贝。
去冗的基本思路:
- 遍历录制数据,识别包含样式表内容的事件(例如快照中的
stylesheet数据、增量中的样式变更); - 将这部分内容提取出来单独保存一份(按内容哈希去重);
- 回放时再通过映射关系把去冗后的内容还原回事件中。
同样的思路也适用于全量快照(FullSnapshot):不同 session 的首次全量快照如果来自同一页面骨架,其 DOM 结构大部分相同,可以跨 session 提取公共部分仅存储一份,显著降低存储总量。
组合应用:一套可落地的优化路线
四类策略并非互斥,实际项目中建议按"先减量、再减频、后减体积"的顺序组合:
- 屏蔽:对长列表、SVG、JS 动画、canvas 动画等"数据大户"用
blockClass/blockSelector/ignoreClass排除出录制范围(packages/rrweb-snapshot/src/snapshot.ts 是判断的核心); - 抽样:用
sampling关闭或降频次要事件——mousemove: false、mouseInteraction细粒度关闭、scroll/media调大节流阈值、input: 'last'(各观察器实现在 packages/rrweb/src/record/observer.ts); - 压缩:客户端可用
packFn(packages/packer/src/pack.ts)做兜底;服务端对整段 session 批量压缩以获得更优压缩比; - 去冗:在存储层对样式表与全量快照做内容级去重。
每一步优化都会带来回放精细度或实现复杂度的代价:屏蔽会导致部分区域回放不可见、input: 'last'会丢失逐键过程、批量压缩需要在回放前先整体解压。建议在接入时先评估真实数据量分布(哪些事件占比最高),再有针对性地启用对应策略,并在每个阶段用真实录制数据验证回放效果,最终得到数据量与回放质量的最优平衡点。
- 前端
- 可观测性
- 开发工具
【免费下载链接】rrweb
record and replay the web
相关推荐
rrweb 存储优化实战:从采样降频到压缩去重的完整方案
rrweb 存储优化实战:从采样降频到压缩去重的完整方案 在真实业务中接入 rrweb(record and replay the web)后,一个常见问题是"
前端可观测性开发工具Docker-Mailserver存储优化终极指南:从压缩到去重的完整解决方案
Docker Mailserver存储优化终极指南:从压缩到去重的完整解决方案 Docker Mailserver作为一款生产级的邮件服务器容器解决方案,集成了
后端通信云原生抽样(Sampling)完全指南:抽样方法、抽样分布、中心极限定理与 Bootstrap
抽样(Sampling)完全指南:抽样方法、抽样分布、中心极限定理与 Bootstrap 本篇为开源教科书 Maths, CS & AI Compendium
文档教程知识库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考