diff --git a/.changeset/hungry-dodos-taste.md b/.changeset/hungry-dodos-taste.md new file mode 100644 index 0000000000..76217d072f --- /dev/null +++ b/.changeset/hungry-dodos-taste.md @@ -0,0 +1,10 @@ +--- +'rrweb-snapshot': patch +--- + +Avoid recreating the same element every time, instead, we cache and we just update the element. + +Before: 779k ops/s +After: 860k ops/s + +Benchmark: https://jsbench.me/ktlqztuf95/1 diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index d323e1af8c..822fa04967 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -196,23 +196,31 @@ function getAbsoluteSrcsetString(doc: Document, attributeValue: string) { return output.join(', '); } +const cachedDocument = new WeakMap(); + export function absoluteToDoc(doc: Document, attributeValue: string): string { if (!attributeValue || attributeValue.trim() === '') { return attributeValue; } - const a: HTMLAnchorElement = doc.createElement('a'); - a.href = attributeValue; - return a.href; + + return getHref(doc, attributeValue); } function isSVGElement(el: Element): boolean { return Boolean(el.tagName === 'svg' || (el as SVGElement).ownerSVGElement); } -function getHref() { - // return a href without hash - const a = document.createElement('a'); - a.href = ''; +function getHref(doc: Document, customHref?: string) { + let a = cachedDocument.get(doc); + if (!a) { + a = doc.createElement('a'); + cachedDocument.set(doc, a); + } + if (!customHref) { + customHref = ''; + } + // note: using `new URL` is slower. See #1434 or https://jsbench.me/uqlud17rxo/1 + a.setAttribute('href', customHref); return a.href; } @@ -244,7 +252,7 @@ export function transformAttribute( } else if (name === 'srcset') { return getAbsoluteSrcsetString(doc, value); } else if (name === 'style') { - return absoluteToStylesheet(value, getHref()); + return absoluteToStylesheet(value, getHref(doc)); } else if (tagName === 'object' && name === 'data') { return absoluteToDoc(doc, value); } @@ -506,6 +514,7 @@ function serializeNode( }); case n.TEXT_NODE: return serializeTextNode(n as Text, { + doc, needsMask, maskTextFn, rootId, @@ -536,6 +545,7 @@ function getRootId(doc: Document, mirror: Mirror): number | undefined { function serializeTextNode( n: Text, options: { + doc: Document; needsMask: boolean | undefined; maskTextFn: MaskTextFn | undefined; rootId: number | undefined; @@ -567,7 +577,7 @@ function serializeTextNode( n, ); } - textContent = absoluteToStylesheet(textContent, getHref()); + textContent = absoluteToStylesheet(textContent, getHref(options.doc)); } if (isScript) { textContent = 'SCRIPT_PLACEHOLDER'; @@ -661,7 +671,7 @@ function serializeElementNode( (n as HTMLStyleElement).sheet as CSSStyleSheet, ); if (cssText) { - attributes._cssText = absoluteToStylesheet(cssText, getHref()); + attributes._cssText = absoluteToStylesheet(cssText, getHref(doc)); } } // form fields