Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(perf): improve perf of mutation and snapshot #1271

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
cc90576
perf(snapshot): optimize code flow of slimDomExcluded
JonasBa Jul 28, 2023
18ca335
perf(snapshot): remove lowerIfExists
JonasBa Jul 28, 2023
ba66b77
perf(snapshot): return early
JonasBa Jul 28, 2023
dc4c7b1
perf(snapshot): test
JonasBa Jul 29, 2023
85b23bf
perf(mutation): shift n^2
JonasBa Jul 29, 2023
bad990b
perf(mutation): improve process
JonasBa Jul 30, 2023
5d59621
fix(mutation): revert regexp exec
JonasBa Aug 1, 2023
58ab15c
fix(prettier): apply formatting
JonasBa Aug 1, 2023
011e0d0
Merge branch 'master' into jb/perf/replay
JonasBa Aug 3, 2023
f6c8738
fix: filter condition
JonasBa Aug 3, 2023
e6bb17e
fix: remove unused import
JonasBa Aug 3, 2023
a927384
fix: lint
JonasBa Aug 3, 2023
23512e6
test: update snapshots
JonasBa Aug 3, 2023
510d82d
revert(genAdds): revert queue implementation
JonasBa Aug 7, 2023
fab0d3d
revert(genAdds): revert snapshot
JonasBa Aug 7, 2023
e188253
feat(benchmark): add blockClass and blockSelector benchmarks
JonasBa Aug 10, 2023
12dc788
Merge branch 'master' into jb/perf/replay
JonasBa Aug 10, 2023
8750f7f
Revert "feat(benchmark): add blockClass and blockSelector benchmarks"
JonasBa Aug 10, 2023
5b05e6b
ref(perf): less recursion and a few minor optimizations
JonasBa Aug 10, 2023
074fdd9
Revert "ref(perf): less recursion and a few minor optimizations"
JonasBa Aug 10, 2023
eab4b25
ref(perf): less recursion
JonasBa Aug 10, 2023
eed46b0
fix(lint): format doc
JonasBa Aug 10, 2023
bc2c8f2
Merge branch 'master' into jb/perf/replay
JonasBa Aug 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions packages/rrdom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,11 +367,15 @@ export class Mirror implements IMirror<RRNode> {
// removes the node from idNodeMap
// doesn't remove the node from nodeMetaMap
removeNodeFromMap(n: RRNode) {
const id = this.getId(n);
this.idNodeMap.delete(id);

if (n.childNodes) {
n.childNodes.forEach((childNode) => this.removeNodeFromMap(childNode));
const queue = [n];
while (queue.length > 0) {
const n = queue.pop()!;
const id = this.getId(n);
this.idNodeMap.delete(id);

if (n.childNodes) {
n.childNodes.forEach((childNode) => queue.push(childNode));
}
}
}
has(id: number): boolean {
Expand Down
144 changes: 87 additions & 57 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,10 @@ export function _isBlockedElement(
blockClass: string | RegExp,
blockSelector: string | null,
): boolean {
if (!blockClass && !blockSelector) {
return false;
}
Comment on lines +265 to +267
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙌


try {
if (typeof blockClass === 'string') {
if (element.classList.contains(blockClass)) {
Expand Down Expand Up @@ -311,23 +315,27 @@ export function needMaskingText(
maskTextClass: string | RegExp,
maskTextSelector: string | null,
): boolean {
try {
const el: HTMLElement | null =
node.nodeType === node.ELEMENT_NODE
? (node as HTMLElement)
: node.parentElement;
if (el === null) return false;
if (!maskTextClass && !maskTextSelector) {
return false;
}

const el: HTMLElement | null =
node.nodeType === node.ELEMENT_NODE
? (node as HTMLElement)
: node.parentElement;

if (el === null) return false;
try {
if (typeof maskTextClass === 'string') {
if (el.classList.contains(maskTextClass)) return true;
if (el.closest(`.${maskTextClass}`)) return true;
if (el.matches(`.${maskTextClass} *`)) return true;
} else {
if (classMatchesRegex(el, maskTextClass, true)) return true;
}

if (maskTextSelector) {
if (el.matches(maskTextSelector)) return true;
if (el.closest(maskTextSelector)) return true;
if (el.matches(`${maskTextSelector} *`)) return true;
}
} catch (e) {
//
Expand Down Expand Up @@ -541,8 +549,8 @@ function serializeTextNode(
// The parent node may not be a html element which has a tagName attribute.
// So just let it be undefined which is ok in this use case.
const parentTagName = n.parentNode && (n.parentNode as HTMLElement).tagName;
let textContent = n.textContent;
const isStyle = parentTagName === 'STYLE' ? true : undefined;
let textContent = n.textContent;
const isScript = parentTagName === 'SCRIPT' ? true : undefined;
if (isStyle && textContent) {
try {
Expand All @@ -568,15 +576,22 @@ function serializeTextNode(
if (isScript) {
textContent = 'SCRIPT_PLACEHOLDER';
}

if (
!isStyle &&
!isScript &&
textContent &&
needMaskingText(n, maskTextClass, maskTextSelector)
) {
textContent = maskTextFn
? maskTextFn(textContent)
: textContent.replace(/[\S]/g, '*');
return {
type: NodeType.Text,
textContent:
(maskTextFn
? maskTextFn(textContent)
: textContent.replace(/[\S]/g, '*')) || '',
isStyle,
rootId,
};
}

return {
Expand Down Expand Up @@ -812,24 +827,40 @@ function serializeElementNode(
};
}

function lowerIfExists(
maybeAttr: string | number | boolean | undefined | null,
): string {
if (maybeAttr === undefined || maybeAttr === null) {
return '';
} else {
return (maybeAttr as string).toLowerCase();
}
}
const MS_APPLICATION_TILE_REGEXP = /^msapplication-tile(image|color)$/;
const OG_TWITTER_OR_FB_REGEXP = /^(og|twitter|fb):/;
const OG_TWITTER_REGEXP = /^(og|twitter):/;
const ARTICLE_PRODUCT_REGEXP = /^(article|product):/;

function slimDOMExcluded(
sn: serializedNode,
slimDOMOptions: SlimDOMOptions,
): boolean {
if (slimDOMOptions.comment && sn.type === NodeType.Comment) {
if (sn.type !== NodeType.Element && sn.type !== NodeType.Comment) {
return false;
}

if (sn.type === NodeType.Comment && slimDOMOptions.comment) {
// TODO: convert IE conditional comments to real nodes
return true;
} else if (sn.type === NodeType.Element) {
}

if (sn.type === NodeType.Element) {
/* eslint-disable */
const snAttributeName: string = sn.attributes.name
JonasBa marked this conversation as resolved.
Show resolved Hide resolved
? // @ts-ignore
sn.attributes.name.toLowerCase()
: '';
const snAttributeRel: string = sn.attributes.rel
? // @ts-ignore
sn.attributes.rel.toLowerCase()
: '';
const snAttributeProperty: string = sn.attributes.property
? // @ts-ignore
sn.attributes.property.toLowerCase()
: '';
/* eslint-enable */

if (
slimDOMOptions.script &&
// script tag
Expand All @@ -850,33 +881,30 @@ function slimDOMExcluded(
slimDOMOptions.headFavicon &&
((sn.tagName === 'link' && sn.attributes.rel === 'shortcut icon') ||
(sn.tagName === 'meta' &&
(lowerIfExists(sn.attributes.name).match(
/^msapplication-tile(image|color)$/,
) ||
lowerIfExists(sn.attributes.name) === 'application-name' ||
lowerIfExists(sn.attributes.rel) === 'icon' ||
lowerIfExists(sn.attributes.rel) === 'apple-touch-icon' ||
lowerIfExists(sn.attributes.rel) === 'shortcut icon')))
(snAttributeName === 'application-name' ||
snAttributeRel === 'icon' ||
snAttributeRel === 'apple-touch-icon' ||
snAttributeRel === 'shortcut icon' ||
MS_APPLICATION_TILE_REGEXP.test(snAttributeName))))
) {
return true;
} else if (sn.tagName === 'meta') {
if (
slimDOMOptions.headMetaDescKeywords &&
lowerIfExists(sn.attributes.name).match(/^description|keywords$/)
(snAttributeName === 'description' || snAttributeName === 'keywords')
) {
return true;
} else if (
slimDOMOptions.headMetaSocial &&
(lowerIfExists(sn.attributes.property).match(/^(og|twitter|fb):/) || // og = opengraph (facebook)
lowerIfExists(sn.attributes.name).match(/^(og|twitter):/) ||
lowerIfExists(sn.attributes.name) === 'pinterest')
(OG_TWITTER_OR_FB_REGEXP.test(snAttributeProperty) || // og = opengraph (facebook)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very tiny nitpick: that the comment makes more sense up at the declaration now 🙈

OG_TWITTER_REGEXP.test(snAttributeName))
) {
return true;
} else if (
slimDOMOptions.headMetaRobots &&
(lowerIfExists(sn.attributes.name) === 'robots' ||
lowerIfExists(sn.attributes.name) === 'googlebot' ||
lowerIfExists(sn.attributes.name) === 'bingbot')
(snAttributeName === 'robots' ||
snAttributeName === 'googlebot' ||
snAttributeName === 'bingbot')
) {
return true;
} else if (
Expand All @@ -888,24 +916,23 @@ function slimDOMExcluded(
return true;
} else if (
slimDOMOptions.headMetaAuthorship &&
(lowerIfExists(sn.attributes.name) === 'author' ||
lowerIfExists(sn.attributes.name) === 'generator' ||
lowerIfExists(sn.attributes.name) === 'framework' ||
lowerIfExists(sn.attributes.name) === 'publisher' ||
lowerIfExists(sn.attributes.name) === 'progid' ||
lowerIfExists(sn.attributes.property).match(/^article:/) ||
lowerIfExists(sn.attributes.property).match(/^product:/))
(snAttributeName === 'author' ||
snAttributeName === 'generator' ||
snAttributeName === 'framework' ||
snAttributeName === 'publisher' ||
snAttributeName === 'progid' ||
ARTICLE_PRODUCT_REGEXP.test(snAttributeProperty))
) {
return true;
} else if (
slimDOMOptions.headMetaVerification &&
(lowerIfExists(sn.attributes.name) === 'google-site-verification' ||
lowerIfExists(sn.attributes.name) === 'yandex-verification' ||
lowerIfExists(sn.attributes.name) === 'csrf-token' ||
lowerIfExists(sn.attributes.name) === 'p:domain_verify' ||
lowerIfExists(sn.attributes.name) === 'verify-v1' ||
lowerIfExists(sn.attributes.name) === 'verification' ||
lowerIfExists(sn.attributes.name) === 'shopify-checkout-api-token')
(snAttributeName === 'google-site-verification' ||
snAttributeName === 'yandex-verification' ||
snAttributeName === 'csrf-token' ||
snAttributeName === 'p:domain_verify' ||
snAttributeName === 'verify-v1' ||
snAttributeName === 'verification' ||
snAttributeName === 'shopify-checkout-api-token')
) {
return true;
}
Expand Down Expand Up @@ -1045,6 +1072,7 @@ export function serializeNodeWithId(
) {
preserveWhiteSpace = false;
}

const bypassOptions = {
doc,
mirror,
Expand All @@ -1069,22 +1097,24 @@ export function serializeNodeWithId(
stylesheetLoadTimeout,
keepIframeSrcFn,
};
for (const childN of Array.from(n.childNodes)) {

n.childNodes.forEach((childN) => {
const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
if (serializedChildNode) {
serializedNode.childNodes.push(serializedChildNode);
}
}
});

if (isElement(n) && n.shadowRoot) {
for (const childN of Array.from(n.shadowRoot.childNodes)) {
n.shadowRoot.childNodes.forEach((childN) => {
const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
if (serializedChildNode) {
isNativeShadowDom(n.shadowRoot) &&
(serializedChildNode.isShadow = true);
if (isNativeShadowDom(n.shadowRoot!)) {
serializedChildNode.isShadow = true;
}
serializedNode.childNodes.push(serializedChildNode);
}
}
});
}
}

Expand Down
14 changes: 8 additions & 6 deletions packages/rrweb-snapshot/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,15 @@ export class Mirror implements IMirror<Node> {
// removes the node from idNodeMap
// doesn't remove the node from nodeMetaMap
removeNodeFromMap(n: Node) {
const id = this.getId(n);
this.idNodeMap.delete(id);
const removeQueue = [n];

if (n.childNodes) {
n.childNodes.forEach((childNode) =>
this.removeNodeFromMap(childNode as unknown as Node),
);
while (removeQueue.length) {
const node = removeQueue.pop()!;
this.idNodeMap.delete(this.getId(node));

if (node.childNodes) {
node.childNodes.forEach((c) => removeQueue.push(c));
}
}
}
has(id: number): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,12 @@ exports[`integration tests [html file]: picture-in-frame.html 1`] = `
</body></html>"
`;

exports[`integration tests [html file]: picture-with-inline-onload.html 1`] = `
"<html xmlns=\\"http://www.w3.org/1999/xhtml\\"><head></head><body>
<img src=\\"http://localhost:3030/images/robot.png\\" alt=\\"This is a robot\\" style=\\"opacity: 1;\\" _onload=\\"this.style.opacity=1\\" />
</body></html>"
`;

exports[`integration tests [html file]: preload.html 1`] = `
"<!DOCTYPE html><html lang=\\"en\\"><head>
<meta charset=\\"UTF-8\\" />
Expand Down
Loading