☰
rrweb 网页录制回放原理与实战:从 DOM 快照到用户行为分析
2026/10/7 10:04:15 网站建设 项目流程

1. rrweb 是什么:把网页"录"下来

接触前端久了,你会遇到一个特别尴尬的反馈:"我刚点了那个按钮,页面就是不动",你自己打开页面怎么点都正常,最后只能靠客服截图或者远程桌面去排查。做产品分析的时候更头疼,想知道用户在第几屏产生了困惑、在哪个步骤反复犹豫,靠埋点只能看到点击坐标,完全还原不了当时页面长什么样。这就是 rrweb(Record 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 type="password">如果不做屏蔽,会被明文记录成事件,这就是隐私事故了。好在 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 回放引擎与虚拟时间:让事件的"播放"可控

回放听起来简单:按时间戳一个个执行事件。但浏览器没有"暂停事件流"这么底层的 API,rrweb 在回放引擎中自己实现了虚拟计时器。它会把所有事件按照timestamp排序,然后通过requestAnimationFrame在主循环里判断当前应该执行到哪个事件。暂停、跳转、倍速播放本质上都是修改虚拟时间轴。

这里有一个容易踩的细节:真实用户访问可能在某一步停了十分钟才继续操作,如果回放时真按时间差去播,中间就会一直白屏。所以 rrweb 设了一个阈值,当事件间隔超过一定值(默认是 5 秒),回放时会跳过等待,直接播放下一个事件。如果你想按真实时间轴回放,可以配置useVirtualTime或回调自己控制,但在默认的 rrweb-player 里,长时间空白会被压缩掉,这也符合"想看用户行为,不想看发呆"的诉求。

3. 动手集成:从零开始接入 rrweb

3.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 提供了blockClass:

blockClass: ['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开启 mask
iframe 内内容无法播放iframe 跨域/默认不录制 iframe同源 iframe 可使用recordCrossOriginIframes,跨域无法完全支持
事件时间轴严重跳变用户长时间停留在页面,事件间隔超阈值调整smoothPlay和虚拟时间配置
回放器和原页面样式不一致CSS 资源异步加载、字体未收集开启collectFonts,或确保无关外部 CSS 可用
首屏录制导致页面卡顿启动录制时同步序列化了大量 DOM延迟启动 +emit异步上报,让快照生成不吃主线程
录屏数据占用存储过大没有压缩、没有采样gzip 压缩 + 开启sampling配置

符合真实场景的录屏数据,价值远高于视频截图。搭好基础链路之后,可以继续扩展的方向很多:比如用rrweb事件流做 AI 行为聚类,识别出"用户在填写订单页反复删除又重填"的高风险路径;也可以结合 Playwright 做端到端测试的录屏审计,所有失败用例自动附带回放链接。这套录制回放能力,本质上把"页面发生了什么"从一个只能肉眼观察的黑盒,变成了可分析、可检索、可复现的数据资产。我在实际项目里的体会是:接入 rrweb 本身不难,难的是为业务定制脱敏规则和降噪策略。先跑通最小链路,再把录制能力嵌入监控和客服工单系统,价值会比单纯录屏大得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询