improve rrdom performance (#1127)

* add more check to rrdom to make diff algorithm more robust

* fix: selector match in iframe is case-insensitive

add try catch to some fragile points

* test: increase timeout value for Jest

* improve code style

* fix: failed to execute insertBefore on Node in the diff function

this happens when ids of doctype or html element are changed in the virtual dom

also improve the code quality

* refactor diff function to make the code cleaner

* fix: virtual nodes are passed to plugin's onBuild function

* refactor the diff function and adjust the order of diff work.

* call afterAppend hook in a consistent traversal order

* improve the performance of the "contains" function

reduce the complexity from O(n) to O(logn)
a specific benchmark is needed to add further

* add a real events for benchmark

* refactor: change the data structure of childNodes from array to linked list

* remove legacy code in rrweb package

* update unit tests

* update change log
This commit is contained in:
Yun Feng
2023-02-14 21:54:30 +11:00
committed by GitHub
parent 282c8fa415
commit 3cc4323094
8 changed files with 362 additions and 351 deletions

View File

@@ -9,6 +9,7 @@ import {
BaseRRMediaElementImpl,
BaseRRNode,
IRRDocumentType,
IRRNode,
} from '../src/document';
describe('Basic RRDocument implementation', () => {
@@ -34,6 +35,7 @@ describe('Basic RRDocument implementation', () => {
expect(node.TEXT_NODE).toBe(document.TEXT_NODE);
expect(node.firstChild).toBeNull();
expect(node.lastChild).toBeNull();
expect(node.previousSibling).toBeNull();
expect(node.nextSibling).toBeNull();
expect(node.contains).toBeDefined();
expect(node.appendChild).toBeDefined();
@@ -42,56 +44,103 @@ describe('Basic RRDocument implementation', () => {
expect(node.toString()).toEqual('RRNode');
});
it('can get first child node', () => {
it('can get and set first child node', () => {
const parentNode = new RRNode();
const childNode1 = new RRNode();
const childNode2 = new RRNode();
expect(parentNode.firstChild).toBeNull();
parentNode.childNodes = [childNode1];
parentNode.firstChild = childNode1;
expect(parentNode.firstChild).toBe(childNode1);
parentNode.childNodes = [childNode1, childNode2];
expect(parentNode.firstChild).toBe(childNode1);
parentNode.childNodes = [childNode2, childNode1];
expect(parentNode.firstChild).toBe(childNode2);
parentNode.firstChild = null;
expect(parentNode.firstChild).toBeNull();
});
it('can get last child node', () => {
it('can get and set last child node', () => {
const parentNode = new RRNode();
const childNode1 = new RRNode();
const childNode2 = new RRNode();
expect(parentNode.lastChild).toBeNull();
parentNode.childNodes = [childNode1];
expect(parentNode.lastChild).toBe(childNode1);
parentNode.childNodes = [childNode1, childNode2];
expect(parentNode.lastChild).toBe(childNode2);
parentNode.childNodes = [childNode2, childNode1];
parentNode.lastChild = childNode1;
expect(parentNode.lastChild).toBe(childNode1);
parentNode.lastChild = null;
expect(parentNode.lastChild).toBeNull();
});
it('can get nextSibling', () => {
it('can get and set preSibling', () => {
const node1 = new RRNode();
const node2 = new RRNode();
expect(node1.previousSibling).toBeNull();
node1.previousSibling = node2;
expect(node1.previousSibling).toBe(node2);
node1.previousSibling = null;
expect(node1.previousSibling).toBeNull();
});
it('can get and set nextSibling', () => {
const node1 = new RRNode();
const node2 = new RRNode();
expect(node1.nextSibling).toBeNull();
node1.nextSibling = node2;
expect(node1.nextSibling).toBe(node2);
node1.nextSibling = null;
expect(node1.nextSibling).toBeNull();
});
it('can get childNodes', () => {
const parentNode = new RRNode();
expect(parentNode.childNodes).toBeInstanceOf(Array);
expect(parentNode.childNodes.length).toBe(0);
const childNode1 = new RRNode();
parentNode.firstChild = childNode1;
parentNode.lastChild = childNode1;
expect(parentNode.childNodes).toEqual([childNode1]);
const childNode2 = new RRNode();
expect(parentNode.nextSibling).toBeNull();
expect(childNode1.nextSibling).toBeNull();
childNode1.parentNode = parentNode;
parentNode.childNodes = [childNode1];
expect(childNode1.nextSibling).toBeNull();
childNode2.parentNode = parentNode;
parentNode.childNodes = [childNode1, childNode2];
expect(childNode1.nextSibling).toBe(childNode2);
expect(childNode2.nextSibling).toBeNull();
parentNode.lastChild = childNode2;
childNode1.nextSibling = childNode2;
childNode2.previousSibling = childNode1;
expect(parentNode.childNodes).toEqual([childNode1, childNode2]);
const childNode3 = new RRNode();
parentNode.lastChild = childNode3;
childNode2.nextSibling = childNode3;
childNode3.previousSibling = childNode2;
expect(parentNode.childNodes).toEqual([
childNode1,
childNode2,
childNode3,
]);
});
it('should return whether the node contains another node', () => {
const parentNode = new RRNode();
expect(parentNode.contains(parentNode)).toBeTruthy();
expect(parentNode.contains(null as unknown as IRRNode)).toBeFalsy();
expect(parentNode.contains(undefined as unknown as IRRNode)).toBeFalsy();
expect(parentNode.contains({} as unknown as IRRNode)).toBeFalsy();
expect(
parentNode.contains(new RRDocument().createElement('div')),
).toBeFalsy();
const childNode1 = new RRNode();
const childNode2 = new RRNode();
parentNode.childNodes = [childNode1];
parentNode.firstChild = childNode1;
parentNode.lastChild = childNode1;
childNode1.parentNode = parentNode;
expect(parentNode.contains(childNode1)).toBeTruthy();
expect(parentNode.contains(childNode2)).toBeFalsy();
childNode1.childNodes = [childNode2];
parentNode.lastChild = childNode2;
childNode1.nextSibling = childNode2;
childNode2.previousSibling = childNode1;
childNode2.parentNode = childNode1;
expect(parentNode.contains(childNode1)).toBeTruthy();
expect(parentNode.contains(childNode2)).toBeTruthy();
const childNode3 = new RRNode();
expect(parentNode.contains(childNode3)).toBeFalsy();
childNode2.firstChild = childNode3;
childNode2.lastChild = childNode3;
childNode3.parentNode = childNode2;
expect(parentNode.contains(childNode3)).toBeTruthy();
});
it('should not implement appendChild', () => {
@@ -143,6 +192,7 @@ describe('Basic RRDocument implementation', () => {
expect(node.TEXT_NODE).toBe(document.TEXT_NODE);
expect(node.firstChild).toBeNull();
expect(node.lastChild).toBeNull();
expect(node.previousSibling).toBeNull();
expect(node.nextSibling).toBeNull();
expect(node.contains).toBeDefined();
expect(node.appendChild).toBeDefined();
@@ -282,7 +332,7 @@ describe('Basic RRDocument implementation', () => {
expect(() =>
node.removeChild(node.createElement('div')),
).toThrowErrorMatchingInlineSnapshot(
`"Failed to execute 'removeChild' on 'RRDocument': The RRNode to be removed is not a child of this RRNode."`,
`"Failed to execute 'removeChild' on 'RRNode': The RRNode to be removed is not a child of this RRNode."`,
);
expect(node.removeChild(documentType)).toBe(documentType);
expect(documentType.parentNode).toBeNull();
@@ -369,6 +419,7 @@ describe('Basic RRDocument implementation', () => {
expect(node.TEXT_NODE).toBe(document.TEXT_NODE);
expect(node.firstChild).toBeNull();
expect(node.lastChild).toBeNull();
expect(node.previousSibling).toBeNull();
expect(node.nextSibling).toBeNull();
expect(node.contains).toBeDefined();
expect(node.appendChild).toBeDefined();
@@ -404,6 +455,7 @@ describe('Basic RRDocument implementation', () => {
expect(node.TEXT_NODE).toBe(document.TEXT_NODE);
expect(node.firstChild).toBeNull();
expect(node.lastChild).toBeNull();
expect(node.previousSibling).toBeNull();
expect(node.nextSibling).toBeNull();
expect(node.contains).toBeDefined();
expect(node.appendChild).toBeDefined();
@@ -686,23 +738,39 @@ describe('Basic RRDocument implementation', () => {
const node = document.createElement('div');
expect(node.childNodes.length).toBe(0);
const child1 = document.createComment('span');
const child1 = document.createElement('span');
expect(node.appendChild(child1)).toBe(child1);
expect(node.childNodes[0]).toEqual(child1);
expect(node.childNodes[0]).toBe(child1);
expect(node.firstChild).toBe(child1);
expect(node.lastChild).toBe(child1);
expect(child1.previousSibling).toBeNull();
expect(child1.nextSibling).toBeNull();
expect(child1.parentElement).toBe(node);
expect(child1.parentNode).toBe(node);
expect(child1.ownerDocument).toBe(document);
expect(node.contains(child1)).toBeTruthy();
const child2 = document.createElement('p');
expect(node.appendChild(child2)).toBe(child2);
expect(node.childNodes[1]).toEqual(child2);
expect(node.childNodes[1]).toBe(child2);
expect(node.firstChild).toBe(child1);
expect(node.lastChild).toBe(child2);
expect(child1.previousSibling).toBeNull();
expect(child1.nextSibling).toBe(child2);
expect(child2.previousSibling).toBe(child1);
expect(child2.nextSibling).toBeNull();
expect(child2.parentElement).toBe(node);
expect(child2.parentNode).toBe(node);
expect(child2.ownerDocument).toBe(document);
expect(node.contains(child1)).toBeTruthy();
expect(node.contains(child2)).toBeTruthy();
});
it('can insert new child before an existing child', () => {
const node = document.createElement('div');
const child1 = document.createElement('h1');
const child2 = document.createElement('h2');
const child3 = document.createElement('h3');
expect(() =>
node.insertBefore(node, child1),
).toThrowErrorMatchingInlineSnapshot(
@@ -710,42 +778,119 @@ describe('Basic RRDocument implementation', () => {
);
expect(node.insertBefore(child1, null)).toBe(child1);
expect(node.childNodes[0]).toBe(child1);
expect(node.childNodes.length).toBe(1);
expect(node.firstChild).toBe(child1);
expect(node.lastChild).toBe(child1);
expect(child1.previousSibling).toBeNull();
expect(child1.nextSibling).toBeNull();
expect(child1.parentNode).toBe(node);
expect(child1.parentElement).toBe(node);
expect(child1.ownerDocument).toBe(document);
expect(node.contains(child1)).toBeTruthy();
expect(node.insertBefore(child2, child1)).toBe(child2);
expect(node.childNodes.length).toBe(2);
expect(node.childNodes[0]).toBe(child2);
expect(node.childNodes[1]).toBe(child1);
expect(node.childNodes).toEqual([child2, child1]);
expect(node.firstChild).toBe(child2);
expect(node.lastChild).toBe(child1);
expect(child1.previousSibling).toBe(child2);
expect(child1.nextSibling).toBeNull();
expect(child2.previousSibling).toBeNull();
expect(child2.nextSibling).toBe(child1);
expect(child2.parentNode).toBe(node);
expect(child2.parentElement).toBe(node);
expect(child2.ownerDocument).toBe(document);
expect(node.contains(child2)).toBeTruthy();
expect(node.contains(child1)).toBeTruthy();
expect(node.insertBefore(child3, child1)).toBe(child3);
expect(node.childNodes).toEqual([child2, child3, child1]);
expect(node.firstChild).toBe(child2);
expect(node.lastChild).toBe(child1);
expect(child1.previousSibling).toBe(child3);
expect(child1.nextSibling).toBeNull();
expect(child3.previousSibling).toBe(child2);
expect(child3.nextSibling).toBe(child1);
expect(child2.previousSibling).toBeNull();
expect(child2.nextSibling).toBe(child3);
expect(child3.parentNode).toBe(node);
expect(child3.parentElement).toBe(node);
expect(child3.ownerDocument).toBe(document);
expect(node.contains(child2)).toBeTruthy();
expect(node.contains(child3)).toBeTruthy();
expect(node.contains(child1)).toBeTruthy();
});
it('can remove an existing child', () => {
const node = document.createElement('div');
const child1 = document.createElement('h1');
const child2 = document.createElement('h2');
const child3 = document.createElement('h3');
node.appendChild(child1);
node.appendChild(child2);
expect(node.childNodes.length).toBe(2);
expect(child1.parentNode).toBe(node);
expect(child2.parentNode).toBe(node);
expect(child1.parentElement).toBe(node);
expect(child2.parentElement).toBe(node);
node.appendChild(child3);
expect(node.childNodes).toEqual([child1, child2, child3]);
expect(() =>
node.removeChild(document.createElement('div')),
).toThrowErrorMatchingInlineSnapshot(
`"Failed to execute 'removeChild' on 'RRElement': The RRNode to be removed is not a child of this RRNode."`,
`"Failed to execute 'removeChild' on 'RRNode': The RRNode to be removed is not a child of this RRNode."`,
);
expect(node.removeChild(child1)).toBe(child1);
expect(child1.parentNode).toBeNull();
expect(child1.parentElement).toBeNull();
expect(node.childNodes.length).toBe(1);
// Remove the middle child.
expect(node.removeChild(child2)).toBe(child2);
expect(node.childNodes.length).toBe(0);
expect(node.childNodes).toEqual([child1, child3]);
expect(node.contains(child2)).toBeFalsy();
expect(node.firstChild).toBe(child1);
expect(node.lastChild).toBe(child3);
expect(child1.previousSibling).toBeNull();
expect(child1.nextSibling).toBe(child3);
expect(child3.previousSibling).toBe(child1);
expect(child3.nextSibling).toBeNull();
expect(child2.previousSibling).toBeNull();
expect(child2.nextSibling).toBeNull();
expect(child2.parentNode).toBeNull();
expect(child2.parentElement).toBeNull();
// Remove the previous child.
expect(node.removeChild(child1)).toBe(child1);
expect(node.childNodes).toEqual([child3]);
expect(node.contains(child1)).toBeFalsy();
expect(node.firstChild).toBe(child3);
expect(node.lastChild).toBe(child3);
expect(child3.previousSibling).toBeNull();
expect(child3.nextSibling).toBeNull();
expect(child1.previousSibling).toBeNull();
expect(child1.nextSibling).toBeNull();
expect(child1.parentNode).toBeNull();
expect(child1.parentElement).toBeNull();
node.insertBefore(child1, child3);
expect(node.childNodes).toEqual([child1, child3]);
// Remove the next child.
expect(node.removeChild(child3)).toBe(child3);
expect(node.childNodes).toEqual([child1]);
expect(node.contains(child3)).toBeFalsy();
expect(node.contains(child1)).toBeTruthy();
expect(node.firstChild).toBe(child1);
expect(node.lastChild).toBe(child1);
expect(child1.previousSibling).toBeNull();
expect(child1.nextSibling).toBeNull();
expect(child3.previousSibling).toBeNull();
expect(child3.nextSibling).toBeNull();
expect(child3.parentNode).toBeNull();
expect(child3.parentElement).toBeNull();
// Remove all children.
expect(node.removeChild(child1)).toBe(child1);
expect(node.childNodes).toEqual([]);
expect(node.contains(child1)).toBeFalsy();
expect(node.contains(child2)).toBeFalsy();
expect(node.contains(child3)).toBeFalsy();
expect(node.firstChild).toBeNull();
expect(node.lastChild).toBeNull();
expect(child1.previousSibling).toBeNull();
expect(child1.nextSibling).toBeNull();
expect(child1.parentNode).toBeNull();
expect(child1.parentElement).toBeNull();
});
});
@@ -768,6 +913,7 @@ describe('Basic RRDocument implementation', () => {
expect(node.TEXT_NODE).toBe(document.TEXT_NODE);
expect(node.firstChild).toBeNull();
expect(node.lastChild).toBeNull();
expect(node.previousSibling).toBeNull();
expect(node.nextSibling).toBeNull();
expect(node.contains).toBeDefined();
expect(node.appendChild).toBeDefined();
@@ -803,6 +949,7 @@ describe('Basic RRDocument implementation', () => {
expect(node.TEXT_NODE).toBe(document.TEXT_NODE);
expect(node.firstChild).toBeNull();
expect(node.lastChild).toBeNull();
expect(node.previousSibling).toBeNull();
expect(node.nextSibling).toBeNull();
expect(node.contains).toBeDefined();
expect(node.appendChild).toBeDefined();
@@ -838,6 +985,7 @@ describe('Basic RRDocument implementation', () => {
expect(node.TEXT_NODE).toBe(document.TEXT_NODE);
expect(node.firstChild).toBeNull();
expect(node.lastChild).toBeNull();
expect(node.previousSibling).toBeNull();
expect(node.nextSibling).toBeNull();
expect(node.contains).toBeDefined();
expect(node.appendChild).toBeDefined();
@@ -870,6 +1018,7 @@ describe('Basic RRDocument implementation', () => {
expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE);
expect(node.TEXT_NODE).toBe(document.TEXT_NODE);
expect(node.firstChild).toBeNull();
expect(node.previousSibling).toBeNull();
expect(node.nextSibling).toBeNull();
expect(node.contains).toBeDefined();
expect(node.appendChild).toBeDefined();