Skip to content

Commit 4babf3c

Browse files
committed
Performance oriented refactor focusing on scenario where a large number of child nodes share the same parent
1 parent edbd403 commit 4babf3c

File tree

1 file changed

+108
-93
lines changed

1 file changed

+108
-93
lines changed

packages/rrweb/src/record/mutation.ts

Lines changed: 108 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
isShadowRoot,
77
needMaskingText,
88
maskInputValue,
9-
Mirror,
109
isNativeShadowDom,
1110
getInputType,
1211
toLowerCase,
@@ -173,49 +172,123 @@ export default class MutationBuffer {
173172
const adds: addedNodeMutation[] = [];
174173
const addedIds = new Set<number>();
175174

176-
const getNextId = (n: Node): number | null => {
177-
let ns: Node | null = n;
178-
let nextId: number | null = IGNORED_NODE; // slimDOM: ignored
179-
while (nextId === IGNORED_NODE) {
180-
ns = ns && ns.nextSibling;
181-
nextId = ns && this.mirror.getId(ns);
175+
while (this.mapRemoves.length) {
176+
this.mirror.removeNodeFromMap(this.mapRemoves.shift()!);
177+
}
178+
179+
for (const n of this.movedSet) {
180+
const parentNode = dom.parentNode(n);
181+
if (
182+
parentNode && // can't be removed if it doesn't exist
183+
this.removesSubTreeCache.has(parentNode) &&
184+
!this.movedSet.has(parentNode)
185+
) {
186+
continue;
182187
}
183-
return nextId;
184-
};
185-
const pushAdd = (n: Node) => {
186-
const parent = dom.parentNode(n);
187-
if (!parent) {
188-
return;
188+
this.addedSet.add(n);
189+
}
190+
191+
let n: Node | null = null;
192+
let parentNode: Node | null = null;
193+
let parentId: number | null = null;
194+
let nextSibling: Node | null = null;
195+
let ancestorBad = false;
196+
const missingParents = new Set<Node>();
197+
while (this.addedSet.size) {
198+
if (n !== null && this.addedSet.has(n.previousSibling)) {
199+
// reuse parentNode, parentId, ancestorBad
200+
nextSibling = n; // n is a good next sibling
201+
n = n.previousSibling;
202+
} else {
203+
n = this.addedSet.values().next().value; // pop
204+
205+
while (true) {
206+
parentNode = dom.parentNode(n);
207+
if (this.addedSet.has(parentNode)) {
208+
// start at top of added tree so as not to serialize children before their parents (parentId requirement)
209+
n = parentNode;
210+
continue;
211+
}
212+
break;
213+
}
214+
215+
if (missingParents.has(parentNode)) {
216+
parentNode = null;
217+
} else if (parentNode) {
218+
// we have a new parentNode for a 'row' of DOM children
219+
// perf: we reuse these calculations across all child nodes
220+
221+
ancestorBad =
222+
isSelfOrAncestorInSet(this.droppedSet, parentNode) ||
223+
this.removesSubTreeCache.has(parentNode);
224+
225+
if (ancestorBad && isSelfOrAncestorInSet(this.movedSet, n)) {
226+
// not bad, just moved
227+
ancestorBad = false;
228+
}
229+
230+
if (!inDom(parentNode)) {
231+
ancestorBad = true;
232+
}
233+
234+
while (true) {
235+
nextSibling = n.nextSibling;
236+
if (this.addedSet.has(nextSibling)) {
237+
// keep going as we can't serialize a node before it's next sibling (nextId requirement)
238+
n = nextSibling;
239+
continue;
240+
}
241+
break;
242+
}
243+
244+
parentId = isShadowRoot(parentNode)
245+
? this.mirror.getId(getShadowHost(n))
246+
: this.mirror.getId(parentNode);
247+
248+
// If the node is the direct child of a shadow root, we treat the shadow host as its parent node.
249+
if (
250+
parentId === -1 &&
251+
parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE
252+
) {
253+
const shadowHost = dom.host(parentNode as ShadowRoot);
254+
parentId = this.mirror.getId(shadowHost);
255+
}
256+
}
189257
}
190258

191-
let parentId = isShadowRoot(parent)
192-
? this.mirror.getId(getShadowHost(n))
193-
: this.mirror.getId(parent);
259+
this.addedSet.delete(n); // don't re-iterate
194260

195-
// If the node is the direct child of a shadow root, we treat the shadow host as its parent node.
196-
if (parentId === -1 && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
197-
const shadowHost = dom.host(parent as ShadowRoot);
198-
parentId = this.mirror.getId(shadowHost);
199-
} else if (!inDom(n)) {
200-
return;
261+
if (!parentNode || parentId === -1) {
262+
missingParents.add(n); // ensure any added child nodes can also early-out
263+
continue;
264+
} else if (ancestorBad) {
265+
// it's possible we could unify missingParents and this.droppedSet
266+
// but would need to check the subtleties
267+
this.droppedSet.add(n);
268+
continue;
201269
}
202270

203271
let cssCaptured = false;
204272
if (n.nodeType === Node.TEXT_NODE) {
205-
const parentTag = (parent as Element).tagName;
273+
const parentTag = (parentNode as Element).tagName;
206274
if (parentTag === 'TEXTAREA') {
207275
// genTextAreaValueMutation already called via parent
208-
return;
209-
} else if (parentTag === 'STYLE' && (this.addedSet.has(parent) || addedIds.has(parentId))) {
276+
continue;
277+
} else if (parentTag === 'STYLE' && addedIds.has(parentId)) {
210278
// css content will be recorded via parent's _cssText attribute when
211279
// mutation adds entire <style> element
212280
cssCaptured = true;
213281
}
214282
}
215283

216-
const nextId = getNextId(n);
217-
if (parentId === -1 || nextId === -1) {
218-
return;
284+
let ns: Node | null = n;
285+
let nextId: number | null = IGNORED_NODE; // slimDOM: ignored
286+
while (nextId === IGNORED_NODE) {
287+
ns = ns && ns.nextSibling;
288+
nextId = ns && this.mirror.getId(ns);
289+
}
290+
if (nextId === -1) {
291+
continue;
219292
}
220293
const sn = serializeNodeWithId(n, {
221294
doc: this.doc,
@@ -265,49 +338,6 @@ export default class MutationBuffer {
265338
});
266339
addedIds.add(sn.id);
267340
}
268-
};
269-
270-
while (this.mapRemoves.length) {
271-
this.mirror.removeNodeFromMap(this.mapRemoves.shift()!);
272-
}
273-
274-
for (const n of this.movedSet) {
275-
if (
276-
isParentRemoved(this.removesSubTreeCache, n, this.mirror) &&
277-
!this.movedSet.has(dom.parentNode(n)!)
278-
) {
279-
continue;
280-
}
281-
this.addedSet.add(n);
282-
}
283-
284-
let n = null;
285-
while (this.addedSet.size) {
286-
if (n === null || !this.addedSet.has(n.previousSibling)) {
287-
n = this.addedSet.values().next().value; // pop
288-
while (this.addedSet.has(dom.parentNode(n))) {
289-
// start as high up as we can
290-
n = dom.parentNode(n);
291-
}
292-
while (this.addedSet.has(n.nextSibling)) {
293-
// keep going until we find one that can be pushed now
294-
n = n.nextSibling;
295-
}
296-
} else {
297-
// n will have a good nextSibling
298-
n = n.previousSibling;
299-
}
300-
this.addedSet.delete(n);
301-
if (
302-
!isAncestorInSet(this.droppedSet, n) &&
303-
!isParentRemoved(this.removesSubTreeCache, n, this.mirror)
304-
) {
305-
pushAdd(n);
306-
} else if (isAncestorInSet(this.movedSet, n)) {
307-
pushAdd(n);
308-
} else {
309-
this.droppedSet.add(n);
310-
}
311341
}
312342

313343
const payload = {
@@ -690,33 +720,18 @@ function processRemoves(n: Node, cache: Set<Node>) {
690720
return;
691721
}
692722

693-
function isParentRemoved(removes: Set<Node>, n: Node, mirror: Mirror): boolean {
694-
if (removes.size === 0) return false;
695-
return _isParentRemoved(removes, n, mirror);
696-
}
697-
698-
function _isParentRemoved(
699-
removes: Set<Node>,
700-
n: Node,
701-
_mirror: Mirror,
702-
): boolean {
703-
const node: ParentNode | null = dom.parentNode(n);
704-
if (!node) return false;
705-
return removes.has(node);
706-
}
707-
708-
function isAncestorInSet(set: Set<Node>, n: Node): boolean {
723+
function isSelfOrAncestorInSet(set: Set<Node>, n: Node): boolean {
709724
if (set.size === 0) return false;
710-
return _isAncestorInSet(set, n);
725+
return _isSelfOrAncestorInSet(set, n);
711726
}
712727

713-
function _isAncestorInSet(set: Set<Node>, n: Node): boolean {
728+
function _isSelfOrAncestorInSet(set: Set<Node>, n: Node): boolean {
729+
if (set.has(n)) {
730+
return true;
731+
}
714732
const parent = dom.parentNode(n);
715733
if (!parent) {
716734
return false;
717735
}
718-
if (set.has(parent)) {
719-
return true;
720-
}
721-
return _isAncestorInSet(set, parent);
736+
return _isSelfOrAncestorInSet(set, parent);
722737
}

0 commit comments

Comments
 (0)