diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json
index 98e4547838..9ed387c2be 100644
--- a/packages/rrweb/package.json
+++ b/packages/rrweb/package.json
@@ -5,8 +5,8 @@
"scripts": {
"prepare": "npm run prepack",
"prepack": "npm run bundle",
- "test": "npm run bundle:browser && jest",
- "test:headless": "npm run bundle:browser && PUPPETEER_HEADLESS=true jest",
+ "test": "npm run bundle:browser && jest --testPathIgnorePatterns test/benchmark",
+ "test:headless": "PUPPETEER_HEADLESS=true npm run test",
"test:watch": "PUPPETEER_HEADLESS=true npm run test -- --watch",
"repl": "npm run bundle:browser && node scripts/repl.js",
"dev": "yarn bundle:browser --watch",
@@ -15,7 +15,8 @@
"typings": "tsc -d --declarationDir typings",
"check-types": "tsc -noEmit",
"prepublish": "npm run typings && npm run bundle",
- "lint": "yarn eslint src"
+ "lint": "yarn eslint src",
+ "benchmark": "jest test/benchmark"
},
"repository": {
"type": "git",
diff --git a/packages/rrweb/test/benchmark/replay-fast-forward.test.ts b/packages/rrweb/test/benchmark/replay-fast-forward.test.ts
new file mode 100644
index 0000000000..4e17acdfdf
--- /dev/null
+++ b/packages/rrweb/test/benchmark/replay-fast-forward.test.ts
@@ -0,0 +1,170 @@
+import * as fs from 'fs';
+import * as path from 'path';
+import type { eventWithTime, recordOptions } from '../../src/types';
+import { launchPuppeteer, ISuite } from '../utils';
+
+const suites: Array<{
+ title: string;
+ eval: string;
+ eventsString?: string;
+ times?: number; // defaults to 5
+}> = [
+ {
+ title: 'append 70 x 70 x 70 elements',
+ eval: `
+ () => {
+ return new Promise((resolve) => {
+ const branches = 70;
+ const depth = 3;
+ // append children for the current node
+ function expand(node, depth) {
+ if (depth == 0) return;
+ for (let b = 0; b < branches; b++) {
+ const child = document.createElement('div');
+ node.appendChild(child);
+ expand(child, depth - 1);
+ }
+ }
+ const frag = document.createDocumentFragment();
+ const node = document.createElement('div');
+ expand(node, depth);
+ frag.appendChild(node);
+ document.body.appendChild(frag);
+ resolve();
+ });
+ };
+ `,
+ times: 3,
+ },
+ {
+ title: 'append 1000 elements and reverse their order',
+ eval: `
+ () => {
+ return new Promise(async (resolve) => {
+ const branches = 1000;
+ function waitForTimeout(timeout) {
+ return new Promise((resolve) => setTimeout(() => resolve(), timeout));
+ }
+ const frag = document.createDocumentFragment();
+ const node = document.createElement('div');
+ for (let b = 0; b < branches; b++) {
+ const child = document.createElement('div');
+ node.appendChild(child);
+ child.textContent = b + 1;
+ }
+ frag.appendChild(node);
+ document.body.appendChild(frag);
+ const children = node.children;
+ await waitForTimeout(0);
+ // reverse the order of children
+ for (let i = children.length - 1; i >= 0; i--)
+ node.appendChild(node.children[i]);
+ resolve();
+ });
+ };
+ `,
+ times: 3,
+ },
+];
+
+function avg(v: number[]): number {
+ return v.reduce((prev, cur) => prev + cur, 0) / v.length;
+}
+
+describe('benchmark: replayer fast-forward performance', () => {
+ jest.setTimeout(240000);
+ let code: ISuite['code'];
+ let page: ISuite['page'];
+ let browser: ISuite['browser'];
+
+ beforeAll(async () => {
+ browser = await launchPuppeteer({
+ headless: true,
+ args: ['--disable-dev-shm-usage'],
+ });
+
+ const bundlePath = path.resolve(__dirname, '../../dist/rrweb.min.js');
+ code = fs.readFileSync(bundlePath, 'utf8');
+
+ for (const suite of suites)
+ suite.eventsString = await generateEvents(suite.eval);
+ }, 600_000);
+
+ afterAll(async () => {
+ await browser.close();
+ });
+
+ for (const suite of suites) {
+ it(
+ suite.title,
+ async () => {
+ suite.times = suite.times ?? 5;
+ const durations: number[] = [];
+ for (let i = 0; i < suite.times; i++) {
+ page = await browser.newPage();
+ await page.goto('about:blank');
+ await page.setContent(`
+
+
+ `);
+ const duration = (await page.evaluate(() => {
+ const replayer = new (window as any).rrweb.Replayer(
+ (window as any).events,
+ );
+ const start = Date.now();
+ replayer.play(replayer.getMetaData().totalTime + 100);
+ return Date.now() - start;
+ })) as number;
+ durations.push(duration);
+ await page.close();
+ }
+
+ console.table([
+ {
+ title: suite.title,
+ times: suite.times,
+ duration: avg(durations),
+ durations: durations.join(', '),
+ },
+ ]);
+ },
+ 60_000,
+ );
+ }
+
+ /**
+ * Get the recorded events after the mutation function is executed.
+ */
+ async function generateEvents(mutateNodesFn: string): Promise {
+ const page = await browser.newPage();
+
+ await page.goto('about:blank');
+ await page.setContent(`
+
+ `);
+ const eventsString = (await page.evaluate((mutateNodesFn) => {
+ return new Promise((resolve) => {
+ const events: eventWithTime[] = [];
+ const options: recordOptions = {
+ emit: (event) => {
+ events.push(event);
+ },
+ };
+ const record = (window as any).rrweb.record;
+ record(options);
+ eval(mutateNodesFn)().then(() => {
+ resolve(JSON.stringify(events));
+ });
+ });
+ }, mutateNodesFn)) as string;
+
+ await page.close();
+ return eventsString;
+ }
+});