Files
rrweb/packages/rrweb-snapshot/src/utils.ts
Justin Halsall 65338aaf11 Speed up snapshotting of many new dom nodes (#903)
* Speed up snapshotting of many new dom nodes

By avoiding reflow we shave about 15-25% off our snapshotting time

* Improve newlyAddedElement docs

* Optimize needMaskingText by using el.closest and less recursion

* Serve all rrweb dist files

* Split serializeNode into smaller functions

Makes it easier to profile

* Slow down cpu enhance tracing on fast machines

* Increase timeout

* Perf: only loop through ancestors when they have something to compare to

* Perf: `hasNode` is cheaper than `getMeta`

* Perf: If parents where already checked, no need to do it again

* Perf: reverse for loops are faster

Because they only do the .lenght check once. In this case I don't think we'll see much performance gains if any

* Clean up code

* Perf: check ancestors once with isBlocked

* guessing this might fixes canvas test

* Update packages/rrweb/src/record/observers/canvas/webgl.ts

Co-authored-by: yz-yu <yanzhen@smartx.com>

* Fix #904 (#906)

Properly remove crossorigin attribute

* Bump minimist from 1.2.5 to 1.2.6 (#902)

Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Co-authored-by: yz-yu <yanzhen@smartx.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-01 12:00:00 +08:00

148 lines
3.6 KiB
TypeScript

import {
idNodeMap,
MaskInputFn,
MaskInputOptions,
nodeMetaMap,
IMirror,
serializedNodeWithId,
} from './types';
export function isElement(n: Node): n is Element {
return n.nodeType === n.ELEMENT_NODE;
}
export function isShadowRoot(n: Node): n is ShadowRoot {
const host: Element | null = (n as ShadowRoot)?.host;
return Boolean(host?.shadowRoot === n);
}
export class Mirror implements IMirror<Node> {
private idNodeMap: idNodeMap = new Map();
private nodeMetaMap: nodeMetaMap = new WeakMap();
getId(n: Node | undefined | null): number {
if (!n) return -1;
const id = this.getMeta(n)?.id;
// if n is not a serialized Node, use -1 as its id.
return id ?? -1;
}
getNode(id: number): Node | null {
return this.idNodeMap.get(id) || null;
}
getIds(): number[] {
return Array.from(this.idNodeMap.keys());
}
getMeta(n: Node): serializedNodeWithId | null {
return this.nodeMetaMap.get(n) || null;
}
// removes the node from idNodeMap
// doesn't remove the node from nodeMetaMap
removeNodeFromMap(n: Node) {
const id = this.getId(n);
this.idNodeMap.delete(id);
if (n.childNodes) {
n.childNodes.forEach((childNode) =>
this.removeNodeFromMap((childNode as unknown) as Node),
);
}
}
has(id: number): boolean {
return this.idNodeMap.has(id);
}
hasNode(node: Node): boolean {
return this.nodeMetaMap.has(node);
}
add(n: Node, meta: serializedNodeWithId) {
const id = meta.id;
this.idNodeMap.set(id, n);
this.nodeMetaMap.set(n, meta);
}
replace(id: number, n: Node) {
this.idNodeMap.set(id, n);
}
reset() {
this.idNodeMap = new Map();
this.nodeMetaMap = new WeakMap();
}
}
export function createMirror(): Mirror {
return new Mirror();
}
export function maskInputValue({
maskInputOptions,
tagName,
type,
value,
maskInputFn,
}: {
maskInputOptions: MaskInputOptions;
tagName: string;
type: string | number | boolean | null;
value: string | null;
maskInputFn?: MaskInputFn;
}): string {
let text = value || '';
if (
maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] ||
maskInputOptions[type as keyof MaskInputOptions]
) {
if (maskInputFn) {
text = maskInputFn(text);
} else {
text = '*'.repeat(text.length);
}
}
return text;
}
const ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__';
type PatchedGetImageData = {
[ORIGINAL_ATTRIBUTE_NAME]: CanvasImageData['getImageData'];
} & CanvasImageData['getImageData'];
export function is2DCanvasBlank(canvas: HTMLCanvasElement): boolean {
const ctx = canvas.getContext('2d');
if (!ctx) return true;
const chunkSize = 50;
// get chunks of the canvas and check if it is blank
for (let x = 0; x < canvas.width; x += chunkSize) {
for (let y = 0; y < canvas.height; y += chunkSize) {
const getImageData = ctx.getImageData as PatchedGetImageData;
const originalGetImageData =
ORIGINAL_ATTRIBUTE_NAME in getImageData
? getImageData[ORIGINAL_ATTRIBUTE_NAME]
: getImageData;
// by getting the canvas in chunks we avoid an expensive
// `getImageData` call that retrieves everything
// even if we can already tell from the first chunk(s) that
// the canvas isn't blank
const pixelBuffer = new Uint32Array(
originalGetImageData.call(
ctx,
x,
y,
Math.min(chunkSize, canvas.width - x),
Math.min(chunkSize, canvas.height - y),
).data.buffer,
);
if (pixelBuffer.some((pixel) => pixel !== 0)) return false;
}
}
return true;
}