ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

rrweb 网页录制回放原理与实战:从 DOM 快照到用户行为分析

rrweb 网页录制回放原理与实战:从 DOM 快照到用户行为分析 1. rrweb 是什么把网页录下来接触前端久了你会遇到一个特别尴尬的反馈我刚点了那个按钮页面就是不动你自己打开页面怎么点都正常最后只能靠客服截图或者远程桌面去排查。做产品分析的时候更头疼想知道用户在第几屏产生了困惑、在哪个步骤反复犹豫靠埋点只能看到点击坐标完全还原不了当时页面长什么样。这就是 rrwebRecord and Replay Web要解决的问题它不是用摄像头录屏而是通过浏览器 API 记录页面的状态变化把一次真实访问回放成一段可交互、可检索、可定位的网页录像。我目前负责的 Web 端用户行为分析系统底层就是基于 rrweb 实现的这篇文章就把它的原理、接入方式、性能调优和踩坑经验完整讲一遍。rrweb 适合三类人看一类是做前端监控或用户行为分析的前端工程师一类是需要用真实操作流程来复现 bug 的质量同学还有一类是产品经理和数据分析师——哪怕你不写代码了解一下它能做什么也能知道怎么给研发提靠谱的录屏需求。2. 录制原理拆解快照与增量组成的时间线2.1 序列化 DOM把页面变成可传输的数据很多人第一次用 rrweb 会误以为它在录视频其实不是。视频走的是getDisplayMedia或者 WebRTC采集的是屏幕像素文件大、没法检索也无法定位到具体 DOM 元素。rrweb 的思路完全相反它把页面 DOM 转成 JSON 节点树连样式、属性、文本节点、SVG、Canvas 状态都序列化下来回放的时候再把这些数据重新挂回浏览器。具体到实现rrweb 内部有三个包rrweb-snapshot负责生成与序列化快照rrweb负责录制事件rrweb-player负责回放界面。快照就是某一时刻页面的完整静态描述相当于一张Word 文档的纯文本备份回放时按快照重建整个 DOM 树后续通过增量事件去修改它。这样数据量远小于视频一条用户访问记录往往几十 KB 就能搞定。要注意序列化并不是简单outerHTML那样把 HTML 扔出来。rrweb 需要处理 CSSStyleSheet 跨域主文档的访问权限问题样式规则写法不一样还要对input的 value、canvas的像素做特殊处理。比如input typepassword如果不做屏蔽会被明文记录成事件这就是隐私事故了。好在 rrweb 默认不记录密码值我们后面会讲到配置项。2.2 MutationObserver 与增量事件只记录变化的补丁页面是动态的如果每隔几十毫秒拍一次全量快照数据量会爆炸。rrweb 靠浏览器原生的MutationObserver来监听 DOM 变化DOM 发生了增删改就生成一个 JSON 格式的增量事件。同时用户操作行为鼠标移动、鼠标点击、触摸、滚动、键盘输入通过事件监听器记录成另一类增量事件最后所有事件都是带时间戳的时间戳就是回放的播放进度条。这里面有个很关键的补丁逻辑快照初始化之后页面某个class变了、文本变了、子节点删了rrweb 会通过before和id来定位是哪个节点再应用修改。它内部给节点维护了一套索引触发 MutationObserver 的 record 会携带nextId等信息来保持顺序。因为 Node 的序列化 id 是全局唯一的即使回放时页面状态和真实访问完全同步也不会找错对象。用户操作对应的事件种类很多MouseInteraction里细分了 Click、DblClick、MouseUp、MouseDown、Hover 等Input事件记录了输入值和是否合成键盘事件TouchInteraction对应移动端的 touch 行为Scroll事件记录的是滚动容器的 scrollTop 和 scrollLeft 变化值而不是屏幕坐标。设计上尽量用语义信息代替像素坐标这样换屏回放也不会错乱。2.3 回放引擎与虚拟时间让事件的播放可控回放听起来简单按时间戳一个个执行事件。但浏览器没有暂停事件流这么底层的 APIrrweb 在回放引擎中自己实现了虚拟计时器。它会把所有事件按照timestamp排序然后通过requestAnimationFrame在主循环里判断当前应该执行到哪个事件。暂停、跳转、倍速播放本质上都是修改虚拟时间轴。这里有一个容易踩的细节真实用户访问可能在某一步停了十分钟才继续操作如果回放时真按时间差去播中间就会一直白屏。所以 rrweb 设了一个阈值当事件间隔超过一定值默认是 5 秒回放时会跳过等待直接播放下一个事件。如果你想按真实时间轴回放可以配置useVirtualTime或回调自己控制但在默认的 rrweb-player 里长时间空白会被压缩掉这也符合想看用户行为不想看发呆的诉求。3. 动手集成从零开始接入 rrweb3.1 安装 rrweb 与基础录制代码先用 npm 安装核心库和播放器npm install rrweb rrweb-player引入样式与录制代码import * as rrweb from rrweb; import rrweb-player/dist/rrweb-player.css; let events []; rrweb.record({ emit(event) { events.push(event); // 实际项目中会在这里按批上报而不是一条条发 }, });这个emit就是每产生一个事件都要执行的回调。我在开发环境第一次跑通时最大感受是它非常不动声色——页面该干嘛干嘛录制的所有东西都被塞进events数组里。要停止录制就调用rrweb.stopRecord()它会返回最后产生的 event 快照确保结尾有完整快照。不过千万别为了省事把数组一直存着不处理。我们线上实践下来一次二十分钟的访问如果不压缩不上报events数组的体积可能到几十 MB。建议的方案是每 10 到 30 秒或者每 1000 个事件就把这批次事件序列化、压缩后发到后端。最后用户关闭页面或进入休眠时调用stopRecord()把剩余事件发完。3.2 回放器的三种姿势官方提供了rrweb-player最省心import rrwebPlayer from rrweb-player; new rrwebPlayer({ target: document.getElementById(player), props: { events, width: 1024, height: 600, autoPlay: false, }, });回放器自带 UI有播放/暂停、进度、倍速、实时模式切换基本够用。如果想完全自定义界面可以直接用rrweb底层提供的Replayer类这个类不依赖 UI你只给它一个挂载节点它负责把事件应用到 DOM 上。我们内部做行为分析的看板就是基于Replayer自绘了一个极简回放器方便在回放页面右侧同时展示错误堆栈、网络请求和日志。第三种是服务端回放。因为事件流本身就是数据你完全可以在 Node.js 里把events重新构建成 HTML 片段或截图用于生成自动化报告。不过服务端没有完整浏览器 API很多 Canvas、字体、动画的序列化逻辑跑不起来实际价值有限。我建议除非你有特殊需求否则还是用浏览器端回放。3.3 数据存储与上报别让录屏把服务器压垮事件数据是 JSON可以存在 MongoDB、PostgreSQL 的 JSON 字段也可以直接存对象存储。关键是要做压缩因为批量事件里存在大量重复的 node 数据和空格。我们用 gzip 压过之后体积能降到原来的 20% 左右效果非常明显。为了减少网络开销还可以通过 service worker 离线暂存在用户网络空闲时批量上报。我在一个低流量项目里试过直接在emit里发sendBeacon虽然上报不阻塞页面但高频场景特别是鼠标移动事件非常多每一条都发会导致浏览器卡顿和服务器压力飙升。后来改成事件累积 定时器批量上报 sendBeacon兜底后整个链路稳定多了。事件数量本身可以通过采样配置降低。rrweb 提供了sampling参数rrweb.record({ emit(event) { ... }, sampling: { mousemove: 50, // 50ms 内最多记录一次鼠标移动 mouseInteraction: false, // 是否录制 mouse interaction scroll: 100, }, });这个参数的本质是按事件频次而不是按事件量做截流。mousemove: 50意思是 50ms 内最多采集一次既保留轨迹大概轮廓又不至于每个像素点都生成一个事件。实测在普通页面上能把录制事件数量减少一半以上。4. 进阶实践让录屏真正好用起来4.1 敏感信息脱敏密码、手机号与数据合规录制页面最忌讳的就是把用户输入的密码、身份证、银行卡号明文记下来。rrweb 在快照序列化和增量事件中提供了多个脱敏入口rrweb.record({ emit(event) { ... }, maskAllInputs: true, maskInputOptions: { password: true, email: true, }, maskText: true, });maskAllInputs会默认把所有 input 控件的值替换成*。如果你的页面里有非密码但含敏感数据的输入框比如验证码、身份证输入框可以通过maskInputOptions配合maskInputFn自定义屏蔽逻辑。需要注意的是maskText会把所有文本都打码这会破坏对用户行为的内容分析。我们最终采用的方式是把 mask 范围限定在输入控件和带特定 class 的文本节点上而不是全局 maskText。还有一个必须处理的点如果页面嵌入了第三方脚本很多第三方脚本会产生动态 DOM比如某个客服聊天浮窗每 5 秒刷新一次如果不屏蔽录制里会混入大量与用户操作无关的垃圾事件。rrweb 提供了blockClassblockClass: [ads, chat-widget],给不需要关注的元素加上rr-block类或者把类名传给blockClass连序列化和录制一起忽略。这个配置对降低事件噪音帮助特别大尤其是页面里挂了一堆统计脚本和推荐位内容的场景。4.2 错误堆栈与录屏联动从用户说卡到直接定位只有录屏还不够一定要和前端错误监控打通。常规错误上报只能拿到堆栈和 UA但页面当时长什么样没人知道。我们在接入 rrweb 后设计了一套联动方案在window.onerror和unhandledrejection里捕获异常。错误发生时从 rrweb 事件列表里取出当前录制时间戳lastEvent.timestamp。将错误 ID 与录屏 Session ID 绑定上报到监控平台。排查问题时直接打开这条错误关联的回放链接把进度拖到错误时间点页面状态一目了然。有一次线上反馈弹窗关闭后列表空白正常复现不出来结果回放里看到用户先快速滚动列表再点击弹窗按钮触发了某个列表插件的virtual-offset计算 bug。没有录屏这类交互时序问题很难留意到。时间点对齐还有一个细节业务自己上报错误时最好用Date.now()和服务端时钟对齐不要依赖设备时区。回放时按事件的时间戳偏移量来定位否则你跳到错误点会发现对不上。4.3 性能优化与踩坑记录Canvas、字体与内存在 PC 端中低端机器上录制最怕的是持续采样导致页面掉帧。这里记录几个我实测有效的优化手段关闭不需要的高耗录制能力recordCanvas: false如果页面不是重图形场景默认不要开 Canvas 录制。Canvas 录制会周期性地对 canvas 截图非常费 CPU。控制 CSS 动画录制高频 transform 动画、滚动容器里的 position 变化会增加 MutationObserver 回调触发次数。最好的办法是给无关紧要的动画区域加rr-block类让它完全不进录制。使用plugins注册自定义插件按业务场景决定哪些节点需要特殊快照。内存侧最典型的问题是录久了越用越卡。原因很简单events数组一直持有所有事件引用而且回放器的 Replayer 实例也会保留虚拟 DOM 状态。如果用户单次访问超过 1 小时建议做成会话分段比如按 10 分钟一段生成一条录屏记录中间用stopRecord()截断再重新record()。字体收集collectFonts: true也会导致大量 CSSOM 操作默认一定要关掉除非你的页面设计强依赖特殊字体且需要回放时样式一致。我们开启过一次测试在字体较多的页面上录制首屏耗时直接翻倍。5. 常见问题与排查技巧5.1 录制成功但回放空白先查这三个环节回放空白是最常见的初级问题。我排查下来90% 的原因出在初始快照和后续事件不是同一份数据录制之前 DOM 没有完全加载在DOMContentLoaded之前就调用record()导致初始快照只录了半页。解决办法是确保在页面 ready 之后启动录制或者在emit里拿到首个Meta事件之后再开始后续逻辑。初始快照丢失服务端存取时按 Batch 存但回放时只加载了最后一个 Batch中间断档。rrweb 回放依赖第一条 document 级别的 full snapshot 事件建议在存储时单独维护 full snapshot 事件回放时保证第一条完整快照完整存在。PNG 强缓存导致 Canvas 数据失效开了 Canvas 录制后部分浏览器会阻止 canvas 转 dataURL回放时 Canvas 完全黑屏。遇到这种问题优先关闭recordCanvas或者给相关 canvas 元素加rr-block。5.2 性能占用过高调整四个参数立竿见影如果你的页面录制一开启就掉帧按照下面顺序调整参数/场景推荐设置原因sampling.mousemove100 以上低频记录鼠标轨迹减少高频事件sampling.scroll50 以上滚动事件非常高频可以大幅截流recordCanvasfalseCanvas 录制是最重的开销之一collectFontsfalse字体收集会遍历 FontFaceSet耗时长blockClass给广告位/图表加rr-block直接不进录制彻底降低 CPU 开销还有一个非常容易忽视的问题不要在每次emit回调里做JSON.stringify(events)。每个事件都是一次完整的序列化叠加大对象会产生 GC 压力。正确做法是先累积数组定时批量序列化或者直接用structuredClone转移数据。5.3 常见问题速查表问题现象可能原因处理办法回放中没有鼠标轨迹sampling.mousemove设置过大改为 20-50ms密码框显示为明文没有开启maskInputOptions.password开启 maskiframe 内内容无法播放iframe 跨域/默认不录制 iframe同源 iframe 可使用recordCrossOriginIframes跨域无法完全支持事件时间轴严重跳变用户长时间停留在页面事件间隔超阈值调整smoothPlay和虚拟时间配置回放器和原页面样式不一致CSS 资源异步加载、字体未收集开启collectFonts或确保无关外部 CSS 可用首屏录制导致页面卡顿启动录制时同步序列化了大量 DOM延迟启动 emit异步上报让快照生成不吃主线程录屏数据占用存储过大没有压缩、没有采样gzip 压缩 开启sampling配置符合真实场景的录屏数据价值远高于视频截图。搭好基础链路之后可以继续扩展的方向很多比如用rrweb事件流做 AI 行为聚类识别出用户在填写订单页反复删除又重填的高风险路径也可以结合 Playwright 做端到端测试的录屏审计所有失败用例自动附带回放链接。这套录制回放能力本质上把页面发生了什么从一个只能肉眼观察的黑盒变成了可分析、可检索、可复现的数据资产。我在实际项目里的体会是接入 rrweb 本身不难难的是为业务定制脱敏规则和降噪策略。先跑通最小链路再把录制能力嵌入监控和客服工单系统价值会比单纯录屏大得多。
返回列表