User Behavior Recording Techniques: Video, Screenshot, and DOM Snapshot (rrweb) Comparison and Implementation
This article examines various user behavior recording methods—including WebRTC video capture, canvas-based screenshot recording, and DOM snapshot recording with rrweb—detailing their technical implementations, advantages, limitations, and suitable application scenarios for product analysis, debugging, and automated testing.
Problem Background
In many projects we rely on click and page‑view (PV) tracking to collect user actions, but these points cannot capture contextual usage scenarios needed by product, development, and testing teams.
Product : Need the real usage path to verify that user behavior matches design expectations.
Development : System alerts indicate an error but not the reproduction steps, especially for intermittent issues.
Testing : When users report bugs, the exact steps are often unknown, leading to high communication cost.
Therefore we need a way to record a continuous sequence of user actions—including clicks, scrolls, inputs—and replay them faithfully.
Technical Solutions
2.1 Video Recording (WebRTC)
WebRTC provides real‑time media streams. The relevant APIs are mediaDevices.getDisplayMedia() , new MediaRecorder() , and the ondataavailable event.
Recording flow:
Call mediaDevices.getDisplayMedia() to obtain screen stream after user permission.
Create a new MediaRecorder() for the stream.
Listen to ondataavailable to collect Blob data.
<
template
>
<
video
ref
=
\"playerRef\"
>
</
video
>
<
button
@
click
=
\"handleStart\"
>
开启录制
</
button
>
<
button
@
click
=
\"handlePause\"
>
暂停录制
</
button
>
<
button
@
click
=
\"handleResume\"
>
继续录制
</
button
>
<
button
@
click
=
\"handleStop\"
>
结束录制
</
button
>
<
button
@
click
=
\"handleReplay\"
>
播放录制
</
button
>
<
button
@
click
=
\"handleReset\"
>
重置内容
</
button
>
</
template
>
<
script
lang
=
\"ts\"
setup
>
import
{ ref, reactive }
from
\'vue\'
;
const
playerRef = ref();
const
state = reactive({
mediaRecorder
:
null
as
null
| MediaRecorder,
blobs
: []
as
Blob[],
});
// 开始录制
const
handleStart =
async
() => {
const
stream =
await
navigator.mediaDevices.getDisplayMedia();
state.mediaRecorder =
new
MediaRecorder(stream, {
mimeType
:
\'video/webm\'
,
});
state.mediaRecorder.addEventListener(\'dataavailable\', (e: BlobEvent) => {
state.blobs.push(e.data);
});
state.mediaRecorder?.start();
};
// canvas录制(特殊处理)
const
handleCanvasRecord =
()
=>
{
const
stream = canvas.captureStream(
60
);
// 60 FPS recording
const
recorder =
new
MediaRecorder(stream, {
mimeType
:
\'video/webm;codecs=vp9\'
,
});
recorder.ondataavailable =
(e) =>
{
state.blobs.push(e.data);
};
}
// 暂停录制
const
handlePause =
()
=>
{ state.mediaRecorder?.pause() };
// 继续录制
const
handleResume =
()
=>
{ state.mediaRecorder?.resume() };
// 停止录制
const
handleStop =
()
=>
{ state.mediaRecorder?.stop() };
// 播放录制
const
handleReplay =
()
=>
{
if
(state.blobs.length ===
0
|| !playerRef.value)
return
;
const
blob =
new
Blob(state.blobs, {
type
:
\'video/webm\'
});
playerRef.value.src = URL.createObjectURL(blob);
playerRef.value.play();
};
const
handleReset =
()
=>
{
state.blobs = [];
state.mediaRecorder =
null
;
playerRef.value.src =
null
;
};
const
handleDownload =
()
=>
{
if
(state.blobs.length ===
0
)
return
;
const
blob =
new
Blob(state.blobs, {
type
:
\'video/webm\'
});
const
url = URL.createObjectURL(blob);
const
a =
document
.createElement(\'a\');
a.href = url;
a.style.display = \'none\';
a.download = \'record.webm\';
a.click();
};
</
script
>Issues with this approach:
Requires user consent and visible UI, making recording perceptible.
Cannot mask sensitive data in the video.
Browser compatibility varies.
2.2 Page Screenshot (html2canvas)
By periodically capturing canvas snapshots with html2canvas and playing them back at the same frame rate, we can simulate recording.
Problems:
Canvas cannot capture animations, may produce layout shifts.
High performance cost and large resource size (e.g., 200 KB per image).
Masking elements removes them entirely, affecting layout.
<
template
>
<
el-button
@
click
=
\"handleStart\"
>
开启录制
</
el-button
>
<
el-button
@
click
=
\"handleStop\"
>
停止录制
</
el-button
>
<
el-button
@
click
=
\"handleReplay\"
>
播放录制
</
el-button
>
<
img
:src=
\"state.imgs[state.num ?? 0]\"
/>
</
template
>
<
script
lang
=
\"ts\"
setup
>
import
{ reactive }
from
\'vue\'
;
import
html2canvas
from
\'html2canvas\'
;
const
state = reactive({
visible
:
false
,
imgs
: []
as
string[],
num
:
0
,
recordInterval
:
null
as
any,
replayInterval
:
null
as
any,
});
const
FPS =
30
;
const
interval =
1000
/ FPS;
const
handleStart =
async
() => {
handleReset();
state.recordInterval = setInterval(() => {
if
(state.imgs.length >
100
) {
handleStop();
return
;
}
html2canvas(document.body).then((canvas: any) => {
const
img = canvas.toDataURL();
state.imgs.push(img);
});
}, interval);
};
const
handleStop = () => {
state.recordInterval && clearInterval(state.recordInterval);
};
const
handleReplay =
async
() => {
state.recordInterval && clearInterval(state.recordInterval);
state.num =
0
;
state.visible =
true
;
state.replayInterval = setInterval(() => {
if
(state.num >= state.imgs.length -
1
) {
clearInterval(state.replayInterval);
return
;
}
state.num++;
}, interval);
};
const
handleReset = () => {
state.imgs = [];
state.recordInterval =
null
;
state.replayInterval =
null
;
state.num =
0
;
};2.3 DOM Snapshot Recording (rrweb)
rrweb records DOM changes as JSON snapshots and replays them. It consists of three parts: rrweb‑snapshot (snapshot & rebuild), rrweb (record & replay), and rrweb‑player (UI controls).
Recording process: a full‑page snapshot is taken, then mutation observers and event listeners capture incremental changes.
Replay process: snapshots are rebuilt in a sandboxed iframe, and events are replayed according to timestamps.
<template>
<button @click=\"handleStart\">开启录制</button>
<button @click=\"handleStop\">结束录制</button>
<button @click=\"handleReplay\">播放录制</button>
<div class=\"replay\" ref=\"replayRef\"></div>
</template>
<script lang=\"ts\" setup>
import { reactive, ref } from \"vue\";
import * as rrweb from \"rrweb\";
import rrwebPlayer from \"rrweb-player\";
import \"rrweb-player/dist/style.css\";
const replayRef = ref();
const state = reactive({
events: [] as any[],
stopFn: null as any,
});
const handleStart = () => {
state.stopFn = rrweb.record({
emit(event) {
if (state.events.length > 100) {
// 当事件数量大于 100 时停止录制
handleStop();
} else {
state.events.push(event);
}
},
});
ElMessage(\'开始录制\');
};
const handleStop = () => {
state.stopFn?.();
ElMessage(\'已停止录制\');
};
const handleReplay = () => {
new rrwebPlayer({
target: replayRef.value, // 可以自定义 DOM 元素
props: { events: state.events },
});
};
</script>2.4 Solution Comparison
Aspect
Video Recording
Page Screenshot
DOM Snapshot
Open‑source library
WebRTC native
html2canvas
rrweb
User perception
Visible
Invisible
Invisible
Output size
Large
Large
Relatively small
Compatibility
Depends on API support
Partial
Good
Operability
Weak
Weak
Strong (supports masking, encryption)
Replay fidelity
Lossy, set at recording
Lossy, set at recording
High fidelity
Application Scenarios
#
Scenario
Description
1
Product feature analysis
Record real user paths to evaluate feature usage and guide optimization.
2
User interview recording
Capture actual user interactions during interviews for later review.
3
Issue reproduction
Preserve the exact steps leading to a bug to reduce communication overhead.
4
Automated test case generation
Convert recorded actions into test scripts.
5
Other
Case review, behavior monitoring, process quality inspection, etc.
Platform Solutions
Sentry and Hotjar both provide recording & replay features built on rrweb, with additional analytics such as heatmaps.
Conclusion
Dom‑snapshot recording with rrweb offers the best balance of fidelity, size, and operability compared with video capture and canvas screenshots, and is widely adopted in commercial solutions.
References
User behavior recording techniques – Juejin
Frontend recording and replay system – Juejin
Browser recording research – Zhihu
WebRTC quick start – Juejin
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.