Cache addHoverClass as it is quite expensive (#643)

* Add cache and cache purging

Needed for https://github.com/rrweb-io/rrweb-snapshot/pull/85

* Add cache and cache purging

Needed for https://github.com/rrweb-io/rrweb-snapshot/pull/85

* Cache addHoverClass as it is quite expensive

https://github.com/rrweb-io/rrweb-snapshot/pull/85

* Make cache non-optional

* Make cache required on addHoverClass
This commit is contained in:
Justin Halsall
2021-07-26 16:13:03 +02:00
committed by GitHub
parent bdd869506c
commit 588164aa12
8 changed files with 122 additions and 26 deletions

View File

@@ -6,7 +6,11 @@ import snapshot, {
needMaskingText,
IGNORED_NODE,
} from './snapshot';
import rebuild, { buildNodeWithSN, addHoverClass } from './rebuild';
import rebuild, {
buildNodeWithSN,
addHoverClass,
createCache,
} from './rebuild';
export * from './types';
export * from './utils';
@@ -16,6 +20,7 @@ export {
rebuild,
buildNodeWithSN,
addHoverClass,
createCache,
transformAttribute,
visitSnapshot,
cleanupSnapshot,

View File

@@ -6,6 +6,7 @@ import {
elementNode,
idNodeMap,
INode,
BuildCache,
} from './types';
import { isElement } from './utils';
@@ -64,7 +65,10 @@ function escapeRegExp(str: string) {
const HOVER_SELECTOR = /([^\\]):hover/;
const HOVER_SELECTOR_GLOBAL = new RegExp(HOVER_SELECTOR, 'g');
export function addHoverClass(cssText: string): string {
export function addHoverClass(cssText: string, cache: BuildCache): string {
const cachedStyle = cache?.stylesWithHoverClass.get(cssText);
if (cachedStyle) return cachedStyle;
const ast = parse(cssText, {
silent: true,
});
@@ -99,10 +103,19 @@ export function addHoverClass(cssText: string): string {
'g',
);
return cssText.replace(selectorMatcher, (selector) => {
const result = cssText.replace(selectorMatcher, (selector) => {
const newSelector = selector.replace(HOVER_SELECTOR_GLOBAL, '$1.\\:hover');
return `${selector}, ${newSelector}`;
});
cache?.stylesWithHoverClass.set(cssText, result);
return result;
}
export function createCache(): BuildCache {
const stylesWithHoverClass: Map<string, string> = new Map();
return {
stylesWithHoverClass,
};
}
function buildNode(
@@ -110,9 +123,10 @@ function buildNode(
options: {
doc: Document;
hackCss: boolean;
cache: BuildCache;
},
): Node | null {
const { doc, hackCss } = options;
const { doc, hackCss, cache } = options;
switch (n.type) {
case NodeType.Document:
return doc.implementation.createDocument(null, '', null);
@@ -143,7 +157,7 @@ function buildNode(
const isRemoteOrDynamicCss =
tagName === 'style' && name === '_cssText';
if (isRemoteOrDynamicCss && hackCss) {
value = addHoverClass(value);
value = addHoverClass(value, cache);
}
if (isTextarea || isRemoteOrDynamicCss) {
const child = doc.createTextNode(value);
@@ -256,7 +270,9 @@ function buildNode(
return node;
case NodeType.Text:
return doc.createTextNode(
n.isStyle && hackCss ? addHoverClass(n.textContent) : n.textContent,
n.isStyle && hackCss
? addHoverClass(n.textContent, cache)
: n.textContent,
);
case NodeType.CDATA:
return doc.createCDATASection(n.textContent);
@@ -275,10 +291,18 @@ export function buildNodeWithSN(
skipChild?: boolean;
hackCss: boolean;
afterAppend?: (n: INode) => unknown;
cache: BuildCache;
},
): INode | null {
const { doc, map, skipChild = false, hackCss = true, afterAppend } = options;
let node = buildNode(n, { doc, hackCss });
const {
doc,
map,
skipChild = false,
hackCss = true,
afterAppend,
cache,
} = options;
let node = buildNode(n, { doc, hackCss, cache });
if (!node) {
return null;
}
@@ -310,6 +334,7 @@ export function buildNodeWithSN(
skipChild: false,
hackCss,
afterAppend,
cache,
});
if (!childNode) {
console.warn('Failed to rebuild', childN);
@@ -369,9 +394,10 @@ function rebuild(
onVisit?: (node: INode) => unknown;
hackCss?: boolean;
afterAppend?: (n: INode) => unknown;
cache: BuildCache;
},
): [Node | null, idNodeMap] {
const { doc, onVisit, hackCss = true, afterAppend } = options;
const { doc, onVisit, hackCss = true, afterAppend, cache } = options;
const idNodeMap: idNodeMap = {};
const node = buildNodeWithSN(n, {
doc,
@@ -379,6 +405,7 @@ function rebuild(
skipChild: false,
hackCss,
afterAppend,
cache,
});
visit(idNodeMap, (visitedNode) => {
if (onVisit) {

View File

@@ -111,3 +111,7 @@ export type MaskTextFn = (text: string) => string;
export type MaskInputFn = (text: string) => string;
export type KeepIframeSrcFn = (src: string) => boolean;
export type BuildCache = {
stylesWithHoverClass: Map<string, string>;
};