Skip to content

Commit 07ac5c9

Browse files
authored
Masking: Avoid the repeated calls to closest when recursing through the DOM (#1349)
* masking performance: avoid the repeated calls to `closest` when recursing through the DOM - needsMask===true means that an ancestor has tested positively for masking, and so this node and all descendents should be masked - needsMask===false means that no ancestors have tested positively for masking, we should check each node encountered - needsMask===undefined means that we don't know whether ancestors are masked or not (e.g. after a mutation) and should look up the tree * Add tests including an explicit characterData mutation tests * Further performance improvement: avoid calls to `el.matches` when on a leaf node, e.g. a `<br/>` --------- Authored-by: eoghanmurray <[email protected]> Based on initial PR #1338 by Alexey Babik <[email protected]>
1 parent 8aea5b0 commit 07ac5c9

File tree

6 files changed

+323
-24
lines changed

6 files changed

+323
-24
lines changed

.changeset/thin-vans-applaud.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'rrweb-snapshot': patch
3+
'rrweb': patch
4+
---
5+
6+
Snapshot performance when masking text: Avoid the repeated calls to `closest` when recursing through the DOM

packages/rrweb-snapshot/src/snapshot.ts

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -310,24 +310,29 @@ export function needMaskingText(
310310
node: Node,
311311
maskTextClass: string | RegExp,
312312
maskTextSelector: string | null,
313+
checkAncestors: boolean,
313314
): boolean {
314315
try {
315316
const el: HTMLElement | null =
316317
node.nodeType === node.ELEMENT_NODE
317318
? (node as HTMLElement)
318319
: node.parentElement;
319320
if (el === null) return false;
320-
321321
if (typeof maskTextClass === 'string') {
322-
if (el.classList.contains(maskTextClass)) return true;
323-
if (el.closest(`.${maskTextClass}`)) return true;
322+
if (checkAncestors) {
323+
if (el.closest(`.${maskTextClass}`)) return true;
324+
} else {
325+
if (el.classList.contains(maskTextClass)) return true;
326+
}
324327
} else {
325-
if (classMatchesRegex(el, maskTextClass, true)) return true;
328+
if (classMatchesRegex(el, maskTextClass, checkAncestors)) return true;
326329
}
327-
328330
if (maskTextSelector) {
329-
if (el.matches(maskTextSelector)) return true;
330-
if (el.closest(maskTextSelector)) return true;
331+
if (checkAncestors) {
332+
if (el.closest(maskTextSelector)) return true;
333+
} else {
334+
if (el.matches(maskTextSelector)) return true;
335+
}
331336
}
332337
} catch (e) {
333338
//
@@ -426,8 +431,7 @@ function serializeNode(
426431
mirror: Mirror;
427432
blockClass: string | RegExp;
428433
blockSelector: string | null;
429-
maskTextClass: string | RegExp;
430-
maskTextSelector: string | null;
434+
needsMask: boolean | undefined;
431435
inlineStylesheet: boolean;
432436
maskInputOptions: MaskInputOptions;
433437
maskTextFn: MaskTextFn | undefined;
@@ -447,8 +451,7 @@ function serializeNode(
447451
mirror,
448452
blockClass,
449453
blockSelector,
450-
maskTextClass,
451-
maskTextSelector,
454+
needsMask,
452455
inlineStylesheet,
453456
maskInputOptions = {},
454457
maskTextFn,
@@ -500,8 +503,7 @@ function serializeNode(
500503
});
501504
case n.TEXT_NODE:
502505
return serializeTextNode(n as Text, {
503-
maskTextClass,
504-
maskTextSelector,
506+
needsMask,
505507
maskTextFn,
506508
rootId,
507509
});
@@ -531,13 +533,12 @@ function getRootId(doc: Document, mirror: Mirror): number | undefined {
531533
function serializeTextNode(
532534
n: Text,
533535
options: {
534-
maskTextClass: string | RegExp;
535-
maskTextSelector: string | null;
536+
needsMask: boolean | undefined;
536537
maskTextFn: MaskTextFn | undefined;
537538
rootId: number | undefined;
538539
},
539540
): serializedNode {
540-
const { maskTextClass, maskTextSelector, maskTextFn, rootId } = options;
541+
const { needsMask, maskTextFn, rootId } = options;
541542
// The parent node may not be a html element which has a tagName attribute.
542543
// So just let it be undefined which is ok in this use case.
543544
const parentTagName = n.parentNode && (n.parentNode as HTMLElement).tagName;
@@ -568,12 +569,7 @@ function serializeTextNode(
568569
if (isScript) {
569570
textContent = 'SCRIPT_PLACEHOLDER';
570571
}
571-
if (
572-
!isStyle &&
573-
!isScript &&
574-
textContent &&
575-
needMaskingText(n, maskTextClass, maskTextSelector)
576-
) {
572+
if (!isStyle && !isScript && textContent && needsMask) {
577573
textContent = maskTextFn
578574
? maskTextFn(textContent, n.parentElement)
579575
: textContent.replace(/[\S]/g, '*');
@@ -935,6 +931,7 @@ export function serializeNodeWithId(
935931
inlineStylesheet: boolean;
936932
newlyAddedElement?: boolean;
937933
maskInputOptions?: MaskInputOptions;
934+
needsMask?: boolean;
938935
maskTextFn: MaskTextFn | undefined;
939936
maskInputFn: MaskInputFn | undefined;
940937
slimDOMOptions: SlimDOMOptions;
@@ -980,14 +977,29 @@ export function serializeNodeWithId(
980977
keepIframeSrcFn = () => false,
981978
newlyAddedElement = false,
982979
} = options;
980+
let { needsMask } = options;
983981
let { preserveWhiteSpace = true } = options;
982+
983+
if (
984+
!needsMask &&
985+
n.childNodes // we can avoid the check on leaf elements, as masking is applied to child text nodes only
986+
) {
987+
// perf: if needsMask = true, children won't also need to check
988+
const checkAncestors = needsMask === undefined; // if false, we've already checked ancestors
989+
needsMask = needMaskingText(
990+
n as Element,
991+
maskTextClass,
992+
maskTextSelector,
993+
checkAncestors,
994+
);
995+
}
996+
984997
const _serializedNode = serializeNode(n, {
985998
doc,
986999
mirror,
9871000
blockClass,
9881001
blockSelector,
989-
maskTextClass,
990-
maskTextSelector,
1002+
needsMask,
9911003
inlineStylesheet,
9921004
maskInputOptions,
9931005
maskTextFn,
@@ -1058,6 +1070,7 @@ export function serializeNodeWithId(
10581070
mirror,
10591071
blockClass,
10601072
blockSelector,
1073+
needsMask,
10611074
maskTextClass,
10621075
maskTextSelector,
10631076
skipChild,
@@ -1118,6 +1131,7 @@ export function serializeNodeWithId(
11181131
mirror,
11191132
blockClass,
11201133
blockSelector,
1134+
needsMask,
11211135
maskTextClass,
11221136
maskTextSelector,
11231137
skipChild: false,
@@ -1165,6 +1179,7 @@ export function serializeNodeWithId(
11651179
mirror,
11661180
blockClass,
11671181
blockSelector,
1182+
needsMask,
11681183
maskTextClass,
11691184
maskTextSelector,
11701185
skipChild: false,

packages/rrweb/src/record/mutation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,7 @@ export default class MutationBuffer {
515515
m.target,
516516
this.maskTextClass,
517517
this.maskTextSelector,
518+
true, // checkAncestors
518519
) && value
519520
? this.maskTextFn
520521
? this.maskTextFn(value, closestElementOfNode(m.target))

0 commit comments

Comments
 (0)