diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index 8c7a81e102..2f2e66c481 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -20,6 +20,7 @@ "check-types": "tsc -noEmit", "prepublish": "tsc -noEmit && vite build", "lint": "yarn eslint src", + "benchmark-dom-mutation": "vitest run --maxConcurrency 1 --no-file-parallelism -t 'deeply nested children' test/benchmark/dom-mutation", "benchmark": "vitest run --maxConcurrency 1 --no-file-parallelism test/benchmark" }, "type": "module", diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index a798441969..eea21502fc 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -680,8 +680,63 @@ export default class MutationBuffer { return; // any removedNodes won't have been in mirror either } - m.addedNodes.forEach((n) => this.genAdds(n, m.target)); - m.removedNodes.forEach((n) => { + const genAddsQueue: [Node, Node | undefined][] = new Array< + [Node, Node | undefined] + >(); + + for (let i = m.addedNodes.length - 1; i >= 0; i--) { + const n = m.addedNodes[i]; + genAddsQueue.push([n, m.target]); + + // iterate breadth first over new nodes (non recursive for performance) + while (genAddsQueue.length) { + const [n, target] = genAddsQueue.pop()!; + + // this node was already recorded in other buffer, ignore it + if (this.processedNodeManager.inOtherBuffer(n, this)) continue; + + // if n is added to set, there is no need to travel it and its' children again + if (this.addedSet.has(n) || this.movedSet.has(n)) continue; + + if (this.mirror.hasNode(n)) { + if (isIgnored(n, this.mirror, this.slimDOMOptions)) { + continue; + } + this.movedSet.add(n); + let targetId: number | null = null; + if (target && this.mirror.hasNode(target)) { + targetId = this.mirror.getId(target); + } + if (targetId && targetId !== -1) { + this.movedMap[moveKey(this.mirror.getId(n), targetId)] = true; + } + } else { + this.addedSet.add(n); + this.droppedSet.delete(n); + } + + if (isBlocked(n, this.blockClass, this.blockSelector, false)) { + // if this node is blocked `serializeNode` will turn it into a placeholder element + // but we have to ignore it's children otherwise they will be added as placeholders too + continue; + } + + for (let j = n.childNodes.length - 1; j >= 0; j--) { + const childN = n.childNodes[j]; + genAddsQueue.push([childN, undefined]); + } + if (hasShadowRoot(n)) { + for (let j = n.shadowRoot.childNodes.length - 1; j >= 0; j--) { + const childN = n.shadowRoot.childNodes[j]; + this.processedNodeManager.add(childN, this); + genAddsQueue.push([childN, n]); + } + } + } + } + + for(let i = 0; i < m.removedNodes.length; i++) { + const n = m.removedNodes[i]; const nodeId = this.mirror.getId(n); const parentId = isShadowRoot(m.target) ? this.mirror.getId(m.target.host) @@ -728,53 +783,13 @@ export default class MutationBuffer { }); } this.mapRemoves.push(n); - }); + }; break; } default: break; } }; - - /** - * Make sure you check if `n`'s parent is blocked before calling this function - * */ - private genAdds = (n: Node, target?: Node) => { - // this node was already recorded in other buffer, ignore it - if (this.processedNodeManager.inOtherBuffer(n, this)) return; - - // if n is added to set, there is no need to travel it and its' children again - if (this.addedSet.has(n) || this.movedSet.has(n)) return; - - if (this.mirror.hasNode(n)) { - if (isIgnored(n, this.mirror, this.slimDOMOptions)) { - return; - } - this.movedSet.add(n); - let targetId: number | null = null; - if (target && this.mirror.hasNode(target)) { - targetId = this.mirror.getId(target); - } - if (targetId && targetId !== -1) { - this.movedMap[moveKey(this.mirror.getId(n), targetId)] = true; - } - } else { - this.addedSet.add(n); - this.droppedSet.delete(n); - } - - // if this node is blocked `serializeNode` will turn it into a placeholder element - // but we have to remove it's children otherwise they will be added as placeholders too - if (!isBlocked(n, this.blockClass, this.blockSelector, false)) { - n.childNodes.forEach((childN) => this.genAdds(childN)); - if (hasShadowRoot(n)) { - n.shadowRoot.childNodes.forEach((childN) => { - this.processedNodeManager.add(childN, this); - this.genAdds(childN, n); - }); - } - } - }; } /** diff --git a/packages/rrweb/src/record/processed-node-manager.ts b/packages/rrweb/src/record/processed-node-manager.ts index c5c3490dab..f68225424b 100644 --- a/packages/rrweb/src/record/processed-node-manager.ts +++ b/packages/rrweb/src/record/processed-node-manager.ts @@ -5,14 +5,11 @@ import type MutationBuffer from './mutation'; */ export default class ProcessedNodeManager { private nodeMap: WeakMap> = new WeakMap(); - private active = false; public inOtherBuffer(node: Node, thisBuffer: MutationBuffer) { const buffers = this.nodeMap.get(node); - return ( - buffers && Array.from(buffers).some((buffer) => buffer !== thisBuffer) - ); + return buffers?.has(thisBuffer) } public add(node: Node, buffer: MutationBuffer) { diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 032bdbec63..2acfd3c164 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -3218,34 +3218,38 @@ exports[`record integration tests > can record node mutations 1`] = ` }, { \\"parentId\\": 26, - \\"nextId\\": 28, + \\"nextId\\": null, \\"node\\": { - \\"type\\": 3, - \\"textContent\\": \\" \\", - \\"id\\": 27 + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": { + \\"class\\": \\"select2-arrow\\", + \\"role\\": \\"presentation\\" + }, + \\"childNodes\\": [], + \\"id\\": 32 } }, { - \\"parentId\\": 26, - \\"nextId\\": 30, + \\"parentId\\": 32, + \\"nextId\\": null, \\"node\\": { \\"type\\": 2, - \\"tagName\\": \\"span\\", + \\"tagName\\": \\"b\\", \\"attributes\\": { - \\"class\\": \\"select2-chosen\\", - \\"id\\": \\"select2-chosen-1\\" + \\"role\\": \\"presentation\\" }, \\"childNodes\\": [], - \\"id\\": 28 + \\"id\\": 33 } }, { - \\"parentId\\": 28, - \\"nextId\\": null, + \\"parentId\\": 26, + \\"nextId\\": 32, \\"node\\": { \\"type\\": 3, - \\"textContent\\": \\"A\\", - \\"id\\": 29 + \\"textContent\\": \\" \\", + \\"id\\": 31 } }, { @@ -3263,38 +3267,34 @@ exports[`record integration tests > can record node mutations 1`] = ` }, { \\"parentId\\": 26, - \\"nextId\\": 32, - \\"node\\": { - \\"type\\": 3, - \\"textContent\\": \\" \\", - \\"id\\": 31 - } - }, - { - \\"parentId\\": 26, - \\"nextId\\": null, + \\"nextId\\": 30, \\"node\\": { \\"type\\": 2, \\"tagName\\": \\"span\\", \\"attributes\\": { - \\"class\\": \\"select2-arrow\\", - \\"role\\": \\"presentation\\" + \\"class\\": \\"select2-chosen\\", + \\"id\\": \\"select2-chosen-1\\" }, \\"childNodes\\": [], - \\"id\\": 32 + \\"id\\": 28 } }, { - \\"parentId\\": 32, + \\"parentId\\": 28, \\"nextId\\": null, \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"b\\", - \\"attributes\\": { - \\"role\\": \\"presentation\\" - }, - \\"childNodes\\": [], - \\"id\\": 33 + \\"type\\": 3, + \\"textContent\\": \\"A\\", + \\"id\\": 29 + } + }, + { + \\"parentId\\": 26, + \\"nextId\\": 28, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 27 } }, { @@ -3314,56 +3314,48 @@ exports[`record integration tests > can record node mutations 1`] = ` }, { \\"parentId\\": 36, - \\"nextId\\": 38, - \\"node\\": { - \\"type\\": 3, - \\"textContent\\": \\" \\", - \\"id\\": 37 - } - }, - { - \\"parentId\\": 36, - \\"nextId\\": 44, + \\"nextId\\": null, \\"node\\": { \\"type\\": 2, - \\"tagName\\": \\"div\\", + \\"tagName\\": \\"ul\\", \\"attributes\\": { - \\"class\\": \\"select2-search\\" + \\"class\\": \\"select2-results\\", + \\"role\\": \\"listbox\\", + \\"id\\": \\"select2-results-1\\" }, \\"childNodes\\": [], - \\"id\\": 38 + \\"id\\": 45 } }, { - \\"parentId\\": 38, - \\"nextId\\": 40, + \\"parentId\\": 36, + \\"nextId\\": 45, \\"node\\": { \\"type\\": 3, - \\"textContent\\": \\" \\", - \\"id\\": 39 + \\"textContent\\": \\" \\", + \\"id\\": 44 } }, { - \\"parentId\\": 38, - \\"nextId\\": 41, + \\"parentId\\": 36, + \\"nextId\\": 44, \\"node\\": { \\"type\\": 2, - \\"tagName\\": \\"label\\", + \\"tagName\\": \\"div\\", \\"attributes\\": { - \\"for\\": \\"s2id_autogen1_search\\", - \\"class\\": \\"select2-offscreen\\" + \\"class\\": \\"select2-search\\" }, \\"childNodes\\": [], - \\"id\\": 40 + \\"id\\": 38 } }, { \\"parentId\\": 38, - \\"nextId\\": 42, + \\"nextId\\": null, \\"node\\": { \\"type\\": 3, - \\"textContent\\": \\" \\", - \\"id\\": 41 + \\"textContent\\": \\" \\", + \\"id\\": 43 } }, { @@ -3393,35 +3385,43 @@ exports[`record integration tests > can record node mutations 1`] = ` }, { \\"parentId\\": 38, - \\"nextId\\": null, + \\"nextId\\": 42, \\"node\\": { \\"type\\": 3, - \\"textContent\\": \\" \\", - \\"id\\": 43 + \\"textContent\\": \\" \\", + \\"id\\": 41 } }, { - \\"parentId\\": 36, - \\"nextId\\": 45, + \\"parentId\\": 38, + \\"nextId\\": 41, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"s2id_autogen1_search\\", + \\"class\\": \\"select2-offscreen\\" + }, + \\"childNodes\\": [], + \\"id\\": 40 + } + }, + { + \\"parentId\\": 38, + \\"nextId\\": 40, \\"node\\": { \\"type\\": 3, - \\"textContent\\": \\" \\", - \\"id\\": 44 + \\"textContent\\": \\" \\", + \\"id\\": 39 } }, { \\"parentId\\": 36, - \\"nextId\\": null, + \\"nextId\\": 38, \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"ul\\", - \\"attributes\\": { - \\"class\\": \\"select2-results\\", - \\"role\\": \\"listbox\\", - \\"id\\": \\"select2-results-1\\" - }, - \\"childNodes\\": [], - \\"id\\": 45 + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 37 } }, { @@ -3463,29 +3463,18 @@ exports[`record integration tests > can record node mutations 1`] = ` } }, { - \\"parentId\\": 18, - \\"nextId\\": 36, + \\"parentId\\": 68, + \\"nextId\\": 69, \\"node\\": { \\"type\\": 2, - \\"tagName\\": \\"div\\", + \\"tagName\\": \\"span\\", \\"attributes\\": { - \\"id\\": \\"select2-drop-mask\\", - \\"class\\": \\"select2-drop-mask\\", - \\"style\\": \\"\\" + \\"class\\": \\"select2-match\\" }, \\"childNodes\\": [], \\"id\\": 70 } }, - { - \\"parentId\\": 62, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 3, - \\"textContent\\": \\"2 results are available, use up and down arrow keys to navigate.\\", - \\"id\\": 71 - } - }, { \\"parentId\\": 45, \\"nextId\\": 67, @@ -3497,11 +3486,11 @@ exports[`record integration tests > can record node mutations 1`] = ` \\"role\\": \\"presentation\\" }, \\"childNodes\\": [], - \\"id\\": 72 + \\"id\\": 71 } }, { - \\"parentId\\": 72, + \\"parentId\\": 71, \\"nextId\\": null, \\"node\\": { \\"type\\": 2, @@ -3512,21 +3501,21 @@ exports[`record integration tests > can record node mutations 1`] = ` \\"role\\": \\"option\\" }, \\"childNodes\\": [], - \\"id\\": 73 + \\"id\\": 72 } }, { - \\"parentId\\": 73, + \\"parentId\\": 72, \\"nextId\\": null, \\"node\\": { \\"type\\": 3, \\"textContent\\": \\"A\\", - \\"id\\": 74 + \\"id\\": 73 } }, { - \\"parentId\\": 73, - \\"nextId\\": 74, + \\"parentId\\": 72, + \\"nextId\\": 73, \\"node\\": { \\"type\\": 2, \\"tagName\\": \\"span\\", @@ -3534,19 +3523,30 @@ exports[`record integration tests > can record node mutations 1`] = ` \\"class\\": \\"select2-match\\" }, \\"childNodes\\": [], - \\"id\\": 75 + \\"id\\": 74 } }, { - \\"parentId\\": 68, - \\"nextId\\": 69, + \\"parentId\\": 18, + \\"nextId\\": 36, \\"node\\": { \\"type\\": 2, - \\"tagName\\": \\"span\\", + \\"tagName\\": \\"div\\", \\"attributes\\": { - \\"class\\": \\"select2-match\\" + \\"id\\": \\"select2-drop-mask\\", + \\"class\\": \\"select2-drop-mask\\", + \\"style\\": \\"\\" }, \\"childNodes\\": [], + \\"id\\": 75 + } + }, + { + \\"parentId\\": 62, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"2 results are available, use up and down arrow keys to navigate.\\", \\"id\\": 76 } } @@ -3576,7 +3576,7 @@ exports[`record integration tests > can record node mutations 1`] = ` \\"data\\": { \\"source\\": 2, \\"type\\": 0, - \\"id\\": 70 + \\"id\\": 75 } }, { @@ -9585,6 +9585,15 @@ exports[`record integration tests > should not record input values if dynamicall \\"id\\": 21 } }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 3, + \\"id\\": 21, + \\"x\\": 2, + \\"y\\": 0 + } + }, { \\"type\\": 3, \\"data\\": { @@ -10689,11 +10698,11 @@ exports[`record integration tests > should record DOM node movement 1 1`] = ` }, { \\"parentId\\": 12, - \\"nextId\\": 14, + \\"nextId\\": null, \\"node\\": { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 13 + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 } }, { @@ -10709,11 +10718,11 @@ exports[`record integration tests > should record DOM node movement 1 1`] = ` }, { \\"parentId\\": 14, - \\"nextId\\": 16, + \\"nextId\\": null, \\"node\\": { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 15 + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 } }, { @@ -10738,20 +10747,20 @@ exports[`record integration tests > should record DOM node movement 1 1`] = ` }, { \\"parentId\\": 14, - \\"nextId\\": null, + \\"nextId\\": 16, \\"node\\": { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 18 + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 } }, { \\"parentId\\": 12, - \\"nextId\\": null, + \\"nextId\\": 14, \\"node\\": { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 19 + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 } } ] @@ -10945,11 +10954,11 @@ exports[`record integration tests > should record DOM node movement 2 1`] = ` \\"adds\\": [ { \\"parentId\\": 12, - \\"nextId\\": 14, + \\"nextId\\": null, \\"node\\": { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 13 + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 } }, { @@ -10965,11 +10974,11 @@ exports[`record integration tests > should record DOM node movement 2 1`] = ` }, { \\"parentId\\": 14, - \\"nextId\\": 16, + \\"nextId\\": null, \\"node\\": { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 15 + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 } }, { @@ -10994,20 +11003,20 @@ exports[`record integration tests > should record DOM node movement 2 1`] = ` }, { \\"parentId\\": 14, - \\"nextId\\": null, + \\"nextId\\": 16, \\"node\\": { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 18 + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 } }, { \\"parentId\\": 12, - \\"nextId\\": null, + \\"nextId\\": 14, \\"node\\": { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 19 + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 } }, { diff --git a/packages/rrweb/test/benchmark/dom-mutation.test.ts b/packages/rrweb/test/benchmark/dom-mutation.test.ts index 96b809e94b..249d92a728 100644 --- a/packages/rrweb/test/benchmark/dom-mutation.test.ts +++ b/packages/rrweb/test/benchmark/dom-mutation.test.ts @@ -89,17 +89,6 @@ describe('benchmark: mutation observer', () => { return fs.readFileSync(filePath, 'utf8'); }; - const addRecordingScript = async (page: Page) => { - // const scriptUrl = `${getServerURL(server)}/rrweb-1.1.3.js`; - const scriptUrl = `${getServerURL(server)}/rrweb.umd.cjs`; - await page.evaluate((url) => { - const scriptEl = document.createElement('script'); - scriptEl.src = url; - document.head.append(scriptEl); - }, scriptUrl); - await page.waitForFunction('window.rrweb'); - }; - for (const suite of suites) { it(suite.title, async () => { page = await browser.newPage(); @@ -110,12 +99,19 @@ describe('benchmark: mutation observer', () => { const loadPage = async () => { if ('html' in suite) { await page.goto('about:blank'); + const code = fs + .readFileSync(path.resolve(__dirname, '../../dist/rrweb.umd.cjs')) + .toString(); + await page.setContent(` + + `); + await page.setContent(getHtml.call(this, suite.html)); } else { await page.goto(suite.url); } - - await addRecordingScript(page); }; const getDuration = async (): Promise => { diff --git a/packages/rrweb/test/html/benchmark-dom-mutation-deep-nested.html b/packages/rrweb/test/html/benchmark-dom-mutation-deep-nested.html index fd0a4258b2..3c448dc6ed 100644 --- a/packages/rrweb/test/html/benchmark-dom-mutation-deep-nested.html +++ b/packages/rrweb/test/html/benchmark-dom-mutation-deep-nested.html @@ -2,7 +2,7 @@