线上工单是程序员最讨厌的东西,尤其是那种“在我这边是好的”“我就是点了几下就出问题了”的描述。之前我排查这类问题只能靠聊天记录截图加后端日志盲猜,偶尔碰上复现步骤带得全的,还得按着用户的操作路径一步步重演,效率低得离谱。后来我把 rrweb 引到项目里,用完整回放去还原用户的操作过程,再配合已有的前端监控做线上 bug 监听,效果完全不一样了。这篇文章我就把整个接入思路、关键配置、回放姿势和踩过的坑都梳理一遍,给同样在做前端稳定性治理的朋友一个可参考的落地模板。
1. 为什么非要用“回放”的方式去定位线上 bug
1.1 光有埋点和日志不够,缺的是“现场”
大多数前端团队现在都上了错误监控,比如把 window.onerror、unhandledrejection 这些全局错误抓到,再附带一个堆栈和 url 上报回来。这套方案能帮我们定位哪一行代码抛了异常,却不能告诉我们用户在那一刻到底触发了什么操作路径,更没法解释为什么同一个页面别人没事、用户偏偏就踩雷。
后端的做法稍微好一点,一般会有请求日志和链路追踪,能看到某个接口响应变慢或者报 500,但用户“怎么一步步走到这个接口”的上下文仍然缺失。比如用户点击了 A 按钮,表格先请求了列表数据,紧接着又触发了一个保存操作,这个先后依赖在前端页面上可能一秒钟就结束了,靠日志是很难还原出来的。
rrweb 这东西补的正是这个“现场”。它不靠手动埋点去编织逻辑,而是通过持续的 DOM 快照和变化记录,把页面上发生的交互过程变成一串可重放的“事件流”。用户当时看见了什么、点了哪里、输入了什么、页面在哪个时刻弹出了报错,全部都能像录像一样在本地回放出来。
1.2 rrweb 记录的是什么,又是怎么做到的
用一句话概括 rrweb 的原理:用录制 DOM 变化事件代替录制视频。它不是一个录屏工具,不靠 canvas.captureStream 去抓画面,而是通过 MutationObserver 监听节点增删改、属性变化,再结合鼠标交互、滚动、输入事件的元数据,生产一连串结构化的记录事件。
这些事件分为几类:
- 全量快照:页面初次加载时记录完整的 DOM 树,作为回放的初始化基线。
- 增量变更:后续每一次节点变更只记录差异,比如某个文本节点从 A 变成 B、某个按钮新增了 disabled 属性。
- 交互事件:鼠标的 mousemove、mousedown、click,以及 input 输入框的字符变化,都会以虚拟坐标和数据值方式记录下来。
- 视图变化:scroll 滚动位置、视口大小变化等。
回放端拿到这些事件后,会先重建一个“轻量文档”,然后把增量变更按时间戳依次 apply 上去,最终把当时的页面状态还原出来。因为保存的是文本和结构而非视频帧,所以单段录制数据的体积相比真正的录屏要小一到两个数量级,这也是它能大规模上生产环境的基础。
1.3 和“录屏软件”的本质区别
有人可能会说,我直接用浏览器录屏不也能排查吗?原生录屏能记录用户电脑上的整个画面,但带来的问题是:数据太大存不了几天、视频不可检索、而且会录进大量与业务无关的信息,隐私风险非常高。
rrweb 的回放是结构化数据,这意味着它可以做三件视频很难做的事情:
- 全文检索:把事件里的文本数据捞出来,比如按用户输入过的关键字去查关联会话。
- 和错误堆栈/接口日志对齐:找到报错发生的时间点,然后精准回放那一段带高亮的操作。
- 按条件抽样或脱敏:URL、文本、图片、输入框内容都可以在录制端做拦截或打码,而不是录完整个视频再去后期处理。
这也是我在实战中最终选择 rrweb 的原因:它不是替代监控,而是把“监控报出一个错误”和“用户实际怎么操作”补成一个完整的因果关系。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 项目整体设计与改造思路
2.1 从短链路到长链路,回放系统拆成哪几块
回到标题说的“使用 rrweb 还原用户的操作,监听线上 BUG”,这个需求可以拆成三条链路:
- 录制链路:在用户浏览器里自动启动记录,产生事件数据,然后缓冲、压缩、上报。
- 存储链路:接收事件数据,建立会话维度索引,关联用户、请求、错误、时间区间等元数据。
- 回放监听链路:开发者在错误面板看到一个异常上报后,点击“查看用户操作录像”,前端从后端拉取该 session 的事件序列,通过 rrweb-player 渲染回放。
在设计这套系统之前,我先确认了几个关键原则,建议你也照着做:
第一,错误优先上报,事件流异步上传。记录数据不要阻塞用户操作,更不能和接口主链路抢带宽。实测我一般会让事件数据走 sendBeacon 或者 requestIdleCallback 延后发送,错误触发时立刻传一次元信息,再补传事件流。
第二,所有事件数据必须带 sessionId 和 timeOffset。sessionId 用来把同一次访问周期的多段记录穿起来,timeOffset 用来重放时对齐页面节点的加载顺序,以及和后端日志时间戳做交叉比对。
第三,录制要能随时启停。不是所有用户都需要全部录制,我采用的是白名单加规则触发:命中某些关键页面、报错发生前的最近 30 秒、或者用户主动反馈问题时,才把完整事件流保留下来。
2.2 技术选型为什么是 rrweb 而不是自己造轮子
有段时间我也想过,能不能在框架层面自己写一套交互记录器,比如在 Vue 的指令里统一埋点 + 把 action 序列存起来。但真做起来会发现三个绕不开的问题。
一是覆盖面不够。你可以在自己写的 onClick 里埋点,但浏览器自动填充、第三方组件内部交互、原生下拉框选择这些行为很难全部埋到,漏一点回放就会失真。rrweb 直接作用在 DOM 变化这一层,不依赖开发者有没有写埋点代码。
二是回放还原成本极高。就算记录下来了点击事件,要把当时的真实界面还原出来,必须解决样式加载、异步状态、图片缓存等一系列时序问题。rrweb 通过快
