
背景
在许多公司的项目矩阵中,通常只有少数核心项目会受到高度关注,特别是生产环境问题的追踪与复现。最近接到一项需求:需要对关键项目进行用户操作行为的记录,核心目标是能够在后台精确还原用户在特定时间点的操作路径与界面状态,以便于问题排查与体验分析。

需求与挑战
要实现这个功能,需要满足以下几个关键点:
- 框架无关性:项目技术栈多样,包括 Vue、Angular、React,方案需要能跨框架适用。
- 行为录制:能够完整捕获用户在页面上的各类交互。
- 录制回放:录制的数据必须能够被重新播放,否则记录失去意义。
- 用户无感知:整个过程需对用户透明,不能干扰其正常操作。
技术选型与方案
提到前端录屏,很自然会想到 WebRTC 技术。然而,采用 WebRTC 方案存在几个明显弊端:
- 无法做到无感知:浏览器会明确请求用户的录制权限,必然会被用户察觉。
- 资源消耗大:录制产生的视频文件体积庞大,占用大量内存与带宽。
- 实现复杂度高:学习与集成成本相对较高。
那么,如何实现真正的无感知且不依赖视频录制呢?关键在于避开“视频”这个媒介。只要不触及浏览器的媒体流 API,就不会触发权限申请。因此,我们选择了 rrweb 这个专门用于录制和回放 Web 界面操作的库。
rrweb 核心介绍
rrweb 是 “record and replay the web” 的缩写,它利用现代浏览器提供的 API,录制并回放任意 Web 界面中的用户操作。
效果展示

基础用法
首先,定义包含操作按钮和回放容器的基本 HTML 结构。
<template>
<div class="main">
<div >
<el-button @click="record">录制</el-button>
<el-button @click="replay">回放</el-button>
<el-button @click="reset">返回</el-button>
</div>
<div v-if="!showReplay">
<div>
<el-input style="width: 300px" v-model="value" />
</div>
<div>
<el-button>按钮1</el-button>
</div>
<div>
<el-button>按钮2</el-button>
</div>
<div>
<el-button>按钮3</el-button>
</div>
</div>
<div ref="replayer"></div>
</div>
</template>
安装核心依赖:npm i rrweb rrwebPlayer。
rrweb:负责录制网页。
rrwebPlayer:负责回放录制内容。
在 JavaScript 逻辑部分,rrweb 通过 record 方法启动录制,其 emit 配置项是一个监听函数,用于接收每一次用户操作产生的数据快照。
const rrweb = require(“rrweb”);
import rrwebPlayer from”rrweb-player”;
const events = ref([]);
const stopFn = ref(null);
const showReplay = ref(false);
const replayer = ref(null)
const record = () => {
console.log(“开始录制”);
stopFn.value = rrweb.record({
emit: (event) => {
events.value.push(event);
},
// 支持录制canvas
recordCanvas: true,
collectFonts: true,
});
};
const replay = () => {
stopFn.value();
showReplay.value = true;
new rrwebPlayer({
target: replayer.value,
props: {
events: events.value,
},
});
};
const reset = () => {
showReplay.value = false;
events.value = [];
};
录制原理:非视频的奥秘
使用 rrweb 录制时,浏览器没有弹出权限请求,这证实了其并非录制视频流。那么它录制的是什么?我们可以输出 emit 回调中的 event 参数一探究竟。
rrweb.record({
emit: (event) => {
// 输出 event 观察
console.log(event)
events.value.push(event);
},
});

从输出可见,event 实质上记录了当前页面 DOM 结构的序列化数据。当用户操作发生时,rrweb 会将 DOM 的增量变化(或全量快照)转换成特定格式的对象。我们用一个数组按顺序收集这些“快照”,然后交由 rrweb-player 按时间线逐帧渲染,从而实现了宛如视频播放般的回放效果。

rrweb 能够录制以下类型的 DOM 行为:
- 节点创建与销毁
- 节点属性变化
- 文本内容变化
- 鼠标移动轨迹
- 鼠标点击、聚焦等交互
- 页面或元素滚动
- 视窗大小改变
- 输入框输入
数据上传与性能优化
录制产生的数据需要上传至服务器端,以便在后台管理系统中进行统一管理与查看。这里有两个重要的优化点:
1. 按需录制与分批上传
不应在全站所有页面开启录制,通常只在核心业务页面启用。同时,为避免频繁请求对浏览器和服务器造成压力,应采用定时分批上传的策略。
const record = () => {
console.log(“开始录制”);
stopFn.value = rrweb.record({
emit: (event) => {
events.value.push(event);
},
recordCanvas: true,
collectFonts: true,
});
};
const report = async () => {
if (events.value.length > 0) {
await reportRequest(events.value); // 上传至后端
events.value = []; // 清空本地缓存
}
}
// 每20秒尝试上传一次
setInterval(report, 20 * 1000);
2. 数据压缩
尽管录制的 DOM 数据比视频小得多,但长时间操作累积的数据量依然可观。

rrweb 内置了数据压缩功能。通过在录制时配置 packFn,在回放时配置对应的 unpackFn,可以显著减小传输数据包的大小,减轻服务器负担。
const record = () => {
console.log(“开始录制”);
stopFn.value = rrweb.record({
emit: (event) => {
events.value.push(event);
},
recordCanvas: true,
collectFonts: true,
packFn: rrweb.pack // 启用压缩
});
};
const replay = () => {
stopFn.value();
showReplay.value = true;
new rrwebPlayer({
target: replayer.value,
props: {
events: events.value,
unpackFn: rrweb.unpack // 对应解压
},
});
};

通过上述方案,我们实现了一套对用户透明、性能可控、支持精准回放的前端用户行为录制系统,为问题定位和产品分析提供了强大的数据支持。