Why Frontend Recording & Replay Matters: Master rrweb for Debugging and Analysis

This article explains why front‑end recording and replay are essential for diagnosing invisible bugs, introduces the open‑source rrweb tool and its core modules, compares it with video and screenshot methods, and provides step‑by‑step guidance for installing, configuring, recording, replaying, and optimizing rrweb in Vue 3 applications.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Why Frontend Recording & Replay Matters: Master rrweb for Debugging and Analysis

Why Front‑End Recording & Replay Is Needed

One of the biggest pain points in front‑end development is the "invisible problem" – when users report issues like unresponsive buttons or blank pages, developers can only rely on limited logs and cannot reproduce the exact user actions.

This information gap slows down troubleshooting and can keep critical business failures affecting users. Front‑end recording and replay provide a full "operation‑state‑result" chain, giving a "god‑view" of the problem and breaking the "hard to trace after the fact" dilemma.

E‑commerce payment : Quickly locate duplicate submissions that cause order loss after payment.

Enterprise SaaS operations : Detect edge cases where rapid tab switching destroys component state before data is saved.

Financial compliance : Record transfer and identity‑verification steps to satisfy audit requirements.

Remote support : Customer service can view the exact failure without relying on user description.

These scenarios share an extreme need for front‑end observability. Traditional monitoring (error logs, performance reports) only provides fragmented data, while recording builds a complete chain, turning "hard to reproduce" into "precisely traceable".

Introducing rrweb: Core Concepts and Features

rrweb (Record and Replay the Web) is an open‑source, lightweight, high‑fidelity, extensible solution for recording and replaying web pages. It captures DOM changes, user interactions, and page state as structured data rather than video streams, making it far more compatible and extensible for frameworks like Vue 3 and React.

rrweb consists of three core modules:

rrweb – the core recording library that captures page changes and events.

rrweb‑snapshot – serializes the DOM into transportable data.

rrweb‑player – a playback component that renders the recorded data.

These modules can be used independently or combined into a full pipeline, which is key to supporting modern framework ecosystems.

Why Choose rrweb Over Traditional Solutions

Traditional video‑based recording suffers from large file sizes, lack of analysis capability, and performance impact. Screenshot stitching reduces size but loses continuity, interaction details, and DOM information.

rrweb’s advantages are:

Lightweight : JSON data is 1/10–1/100 the size of video (≈100 KB–1 MB per minute).

High fidelity : Precisely reproduces DOM changes, network latency, and async component rendering.

Analyzable : Includes full DOM snapshots and event logs, allowing direct jump from symptom to root cause.

Highly compatible : Works in Chrome, Firefox, Safari 10+, and integrates with Vue 3, React, jQuery without invasive code changes.

Quick Start – Basic Recording & Playback

The following steps show how to set up rrweb in a Vue 3 project.

1. Install Dependencies

# Using pnpm (recommended)
pnpm install rrweb rrweb-player rrweb-snapshot
# Using npm
npm install rrweb rrweb-player rrweb-snapshot
Note: Explicitly installing rrweb-snapshot avoids version‑compatibility issues in multi‑developer projects.

2. Import Modules

import { record } from 'rrweb';
import rrwebPlayer from 'rrweb-player';
import 'rrweb-player/dist/style.css';
import { ref } from 'vue';

3. Define Core State Variables

const events = ref([]); // Recorded event array
const recordConfig = ref({
  maskAllInputs: true,
  maskInputPassword: true,
  blockClass: 'rrweb-block',
  recordCanvas: false,
  samplingInterval: 100
});
const isRecording = ref(false);
const hasRecording = ref(false);
let stopFn = null;
let player = null;
const replayContainer = ref(null);

4. Implement Recording Functions

Start Recording

const startRecord = () => {
  events.value = [];
  console.log('### Starting recording, config:', recordConfig.value);
  const options = {
    emit(event) { events.value.push(event); },
    maskAllInputs: recordConfig.value.maskAllInputs,
    maskInputOptions: { password: recordConfig.value.maskInputPassword },
    blockClass: recordConfig.value.blockClass,
    recordCanvas: recordConfig.value.recordCanvas,
    inlineStylesheet: true,
    sampling: recordConfig.value.samplingInterval > 0 ? {
      mousemove: true,
      mouseInteraction: true,
      scroll: recordConfig.value.samplingInterval,
      input: 'all'
    } : { input: 'all' }
  };
  stopFn = record(options);
  isRecording.value = true;
  hasRecording.value = false;
};

Stop Recording

const stopRecord = () => {
  if (!stopFn) return;
  stopFn();
  stopFn = null;
  isRecording.value = false;
  hasRecording.value = events.value.length > 0;
  console.log(`### Stopped recording, collected ${events.value.length} events`);
};

5. Implement Playback

const replay = () => {
  if (events.value.length === 0) {
    console.warn('### No recording data, cannot replay');
    return;
  }
  if (player) {
    player.pause();
    player = null;
  }
  player = new rrwebPlayer({
    target: replayContainer.value,
    props: {
      events: events.value,
      width: replayContainer.value.clientWidth,
      height: replayContainer.value.clientHeight,
      autoPlay: true,
      showController: true,
      speedOption: [1, 2, 4, 8],
      showTime: true
    }
  });
};

6. Vue Template Example

<template>
  <div class="rrweb-demo">
    <div class="control-buttons">
      <button @click="isRecording ? stopRecord() : startRecord()">
        {{ isRecording ? 'Stop Recording' : 'Start Recording' }}
      </button>
      <button @click="replay()" :disabled="!hasRecording">Play Replay</button>
    </div>
    <div class="replay-container" ref="replayContainer"></div>
  </div>
</template>

<style scoped>
.rrweb-demo { padding: 20px; }
.control-buttons { margin-bottom: 20px; display: flex; gap: 10px; }
.control-buttons button { padding: 8px 16px; cursor: pointer; }
.replay-container { width: 100%; height: 600px; border: 1px solid #e5e7eb; border-radius: 4px; overflow: hidden; }
</style>

Advanced Features

Privacy Protection (Input Masking & Element Blocking)

Sensitive data such as ID numbers, bank cards, and passwords must be masked. rrweb offers two mechanisms:

Input Masking : Configure maskAllInputs to mask every input field, or use maskInputOptions to mask only password fields.

Element Blocking : Assign a CSS class (e.g., rrweb-block) to hide entire DOM regions during recording.

const recordConfig = ref({
  maskAllInputs: false,
  maskInputPassword: true,
  blockClass: 'rrweb-block'
});
const options = {
  maskAllInputs: recordConfig.value.maskAllInputs,
  maskInputOptions: { password: recordConfig.value.maskInputPassword },
  blockClass: recordConfig.value.blockClass
};

Sampling Rate Optimization

High‑frequency events (mousemove, scroll) generate massive data. Use samplingInterval to control how often these events are recorded.

const recordConfig = ref({ samplingInterval: 200 }); // 200 ms interval
const options = {
  sampling: recordConfig.value.samplingInterval > 0 ? {
    mousemove: true,
    mouseInteraction: true,
    scroll: recordConfig.value.samplingInterval,
    input: 'all'
  } : { input: 'all' }
};

Recommended interval: 200‑500 ms balances data size and playback smoothness.

Canvas Recording

For pages with dynamic Canvas (games, charts, whiteboards), enable recordCanvas. Be aware of the increased payload; optionally disable inlineImages to keep JSON size manageable.

const recordConfig = ref({ recordCanvas: true, inlineImages: false });
const options = {
  recordCanvas: recordConfig.value.recordCanvas,
  inlineImages: recordConfig.value.inlineImages
};

Data Management (Size Calculation, Download, Upload)

Calculate Recording Size

const dataSize = computed(() => {
  const size = new Blob([JSON.stringify(events.value)]).size;
  if (size < 1024) return `${size} B`;
  if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`;
  return `${(size / 1024 / 1024).toFixed(2)} MB`;
});

Display dataSize next to the recording status to give developers real‑time feedback.

Download Recording Data

const downloadRecording = () => {
  if (events.value.length === 0) {
    ElMessage.warning('No recording data to download');
    return;
  }
  const data = JSON.stringify(events.value, null, 2);
  const blob = new Blob([data], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `rrweb-recording-${Date.now()}.json`;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
  console.log('### Downloaded recording, size:', dataSize.value);
};

Upload Recording Data

Users can import a previously saved JSON file to replay it.

// Trigger hidden file input
const triggerUpload = () => {
  document.getElementById('file-upload').click();
};

// Handle file selection
const uploadRecording = (event) => {
  const file = event.target.files[0];
  if (!file) return;
  if (file.type !== 'application/json' && !file.name.endsWith('.json')) {
    ElMessage.error('Please upload a JSON file');
    event.target.value = '';
    return;
  }
  const reader = new FileReader();
  reader.onload = (e) => {
    try {
      const data = JSON.parse(e.target.result);
      if (!Array.isArray(data) || data.length === 0) {
        throw new Error('Recording data is empty or invalid');
      }
      const hasFullSnapshot = data.some(item => item.type === 2);
      if (!hasFullSnapshot) {
        ElMessage.warning('Recording lacks a full snapshot; playback may fail');
      }
      events.value = data;
      hasRecording.value = true;
      ElMessage.success(`Upload successful! Imported ${data.length} events`);
      console.log('### Uploaded recording size:', new Blob([e.target.result]).size / 1024, 'KB');
    } catch (error) {
      console.error('### Upload failed:', error);
      ElMessage.error(`Upload failed: ${error.message}`);
    }
  };
  reader.onerror = () => {
    ElMessage.error('File read error, please check the file');
  };
  reader.readAsText(file);
  event.target.value = '';
};

Template snippet for upload:

<!-- Hidden file input -->
<input id="file-upload" type="file" accept=".json" style="display:none" @change="uploadRecording" />
<!-- Upload button visible to users -->
<el-button type="primary" icon="el-icon-upload" @click="triggerUpload" :disabled="isRecording">Upload Recording</el-button>

Best Practices Summary

Prefer targeted input masking over global masking to retain useful debugging information.

Use a unique prefix (e.g., rr‑) for block classes to avoid accidental masking of business elements.

Set samplingInterval to 200‑500 ms for most pages; keep input: 'all' to ensure form data is fully captured.

Enable recordCanvas only when dynamic Canvas content must be reproduced.

Show real‑time dataSize and warn when size exceeds a threshold (e.g., 5 MB).

When downloading, use a timestamped filename like rrweb-recording-1716234567890.json.

Validate uploaded files for JSON format, array structure, and presence of a full snapshot before assigning to events.

For large uploads (>5 MB), consider chunked upload with server‑side assembly to avoid memory issues.

debuggingfrontendprivacyVuerecordingreplayrrweb
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.