diff --git a/.changeset/fair-seahorses-care.md b/.changeset/fair-seahorses-care.md new file mode 100644 index 0000000000..f8f0d9da6f --- /dev/null +++ b/.changeset/fair-seahorses-care.md @@ -0,0 +1,6 @@ +--- +'rrweb': patch +--- + +Use native setTimeout when zone.js is present +Use native add/removeEventListener when zone.js is present diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index d323e1af8c..abb872b006 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -25,6 +25,7 @@ import { getInputType, toLowerCase, extractFileExtension, + nativeSetTimeout } from './utils'; let _id = 1; @@ -363,7 +364,7 @@ function onceIframeLoaded( return; } if (readyState !== 'complete') { - const timer = setTimeout(() => { + const timer = nativeSetTimeout(() => { if (!fired) { listener(); fired = true; @@ -385,7 +386,7 @@ function onceIframeLoaded( ) { // iframe was already loaded, make sure we wait to trigger the listener // till _after_ the mutation that found this iframe has had time to process - setTimeout(listener, 0); + nativeSetTimeout(listener, 0); return iframeEl.addEventListener('load', listener); // keep listing for future loads } @@ -413,7 +414,7 @@ function onceStylesheetLoaded( if (styleSheetLoaded) return; - const timer = setTimeout(() => { + const timer = nativeSetTimeout(() => { if (!fired) { listener(); fired = true; diff --git a/packages/rrweb-snapshot/src/types.ts b/packages/rrweb-snapshot/src/types.ts index 1abfe4d6c0..c3a3f335b1 100644 --- a/packages/rrweb-snapshot/src/types.ts +++ b/packages/rrweb-snapshot/src/types.ts @@ -184,3 +184,5 @@ export type KeepIframeSrcFn = (src: string) => boolean; export type BuildCache = { stylesWithHoverClass: Map; }; + +export type IWindow = Window & typeof globalThis; diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 5ccc9082ed..25c8f9acbd 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -11,6 +11,7 @@ import { documentTypeNode, textNode, elementNode, + IWindow, } from './types'; export function isElement(n: Node): n is Element { @@ -30,6 +31,34 @@ export function isNativeShadowDom(shadowRoot: ShadowRoot) { return Object.prototype.toString.call(shadowRoot) === '[object ShadowRoot]'; } +type WindowWithAngularZone = IWindow & { + Zone?: { + __symbol__?: (key: keyof IWindow) => string; + }; + [key: string]: any; +}; + +export function getNative( + symbolName: keyof IWindow, + windowObj: IWindow = window, +): T { + const windowWithZone = windowObj as WindowWithAngularZone; + const angularZoneSymbol = windowWithZone?.Zone?.__symbol__?.(symbolName); + if (angularZoneSymbol) { + const zonelessImpl = windowWithZone[angularZoneSymbol] as T; + if (zonelessImpl) { + return zonelessImpl; + } + } + + return windowWithZone[symbolName] as T; +} + +export const nativeSetTimeout = + typeof window !== 'undefined' + ? getNative('setTimeout') + : global.setTimeout; + /** * Browsers sometimes destructively modify the css rules they receive. * This function tries to rectify the modifications the browser made to make it more cross platform compatible. diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index dcd30e4718..5c8bbb069c 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -4,6 +4,8 @@ import { Mirror, getInputType, toLowerCase, + getNative, + nativeSetTimeout, } from 'rrweb-snapshot'; import type { FontFaceSet } from 'css-font-loading-module'; import { @@ -54,11 +56,6 @@ import { callbackWrapper } from './error-handler'; type WindowWithStoredMutationObserver = IWindow & { __rrMutationObserver?: MutationObserver; }; -type WindowWithAngularZone = IWindow & { - Zone?: { - __symbol__?: (key: string) => string; - }; -}; export const mutationBuffers: MutationBuffer[] = []; @@ -93,7 +90,7 @@ export function initMutationObserver( // see mutation.ts for details mutationBuffer.init(options); let mutationObserverCtor = - window.MutationObserver || + getNative('MutationObserver') || /** * Some websites may disable MutationObserver by removing it from the window object. * If someone is using rrweb to build a browser extention or things like it, they @@ -103,19 +100,6 @@ export function initMutationObserver( * window.__rrMutationObserver = MutationObserver */ (window as WindowWithStoredMutationObserver).__rrMutationObserver; - const angularZoneSymbol = ( - window as WindowWithAngularZone - )?.Zone?.__symbol__?.('MutationObserver'); - if ( - angularZoneSymbol && - (window as unknown as Record)[ - angularZoneSymbol - ] - ) { - mutationObserverCtor = ( - window as unknown as Record - )[angularZoneSymbol]; - } const observer = new (mutationObserverCtor as new ( callback: MutationCallback, ) => MutationObserver)( @@ -1105,7 +1089,7 @@ function initFontObserver({ fontCb, doc }: observerParam): listenerHandler { 'add', function (original: (font: FontFace) => void) { return function (this: FontFaceSet, fontFace: FontFace) { - setTimeout( + nativeSetTimeout( callbackWrapper(() => { const p = fontMap.get(fontFace); if (p) { diff --git a/packages/rrweb/src/record/observers/canvas/2d.ts b/packages/rrweb/src/record/observers/canvas/2d.ts index b496b6b93a..f0c11e5832 100644 --- a/packages/rrweb/src/record/observers/canvas/2d.ts +++ b/packages/rrweb/src/record/observers/canvas/2d.ts @@ -1,4 +1,5 @@ import type { Mirror } from 'rrweb-snapshot'; +import { nativeSetTimeout } from 'rrweb-snapshot'; import { blockClass, CanvasContext, @@ -44,7 +45,7 @@ export default function initCanvas2DMutationObserver( if (!isBlocked(this.canvas, blockClass, blockSelector, true)) { // Using setTimeout as toDataURL can be heavy // and we'd rather not block the main thread - setTimeout(() => { + nativeSetTimeout(() => { const recordArgs = serializeArgs(args, win, this); cb(this.canvas, { type: CanvasContext['2D'], diff --git a/packages/rrweb/src/record/shadow-dom-manager.ts b/packages/rrweb/src/record/shadow-dom-manager.ts index 169c77216a..f27a41abc5 100644 --- a/packages/rrweb/src/record/shadow-dom-manager.ts +++ b/packages/rrweb/src/record/shadow-dom-manager.ts @@ -11,7 +11,7 @@ import { } from './observer'; import { patch, inDom } from '../utils'; import type { Mirror } from 'rrweb-snapshot'; -import { isNativeShadowDom } from 'rrweb-snapshot'; +import { isNativeShadowDom, nativeSetTimeout } from 'rrweb-snapshot'; type BypassOptions = Omit< MutationBufferParam, @@ -74,7 +74,7 @@ export class ShadowDomManager { }), ); // Defer this to avoid adoptedStyleSheet events being created before the full snapshot is created or attachShadow action is recorded. - setTimeout(() => { + nativeSetTimeout(() => { if ( shadowRoot.adoptedStyleSheets && shadowRoot.adoptedStyleSheets.length > 0 diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index f426689d2f..782fd3dd8e 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -10,17 +10,36 @@ import type { textMutation, } from '@rrweb/types'; import type { IMirror, Mirror } from 'rrweb-snapshot'; -import { isShadowRoot, IGNORED_NODE, classMatchesRegex } from 'rrweb-snapshot'; +import { + isShadowRoot, + IGNORED_NODE, + classMatchesRegex, + getNative, + nativeSetTimeout, +} from 'rrweb-snapshot'; import type { RRNode, RRIFrameElement } from 'rrdom'; +function getWindow(documentOrWindow: Document | IWindow): IWindow { + const defaultView = (documentOrWindow as Document).defaultView; + return (defaultView ? defaultView : documentOrWindow) as IWindow; +} + export function on( type: string, fn: EventListenerOrEventListenerObject, target: Document | IWindow = document, ): listenerHandler { + const windowObj = getWindow(target); + const nativeAddEventListener = getNative( + 'addEventListener', + windowObj, + ); + const nativeRemoveEventListener = getNative< + typeof window.removeEventListener + >('removeEventListener', windowObj); const options = { capture: true, passive: true }; - target.addEventListener(type, fn, options); - return () => target.removeEventListener(type, fn, options); + nativeAddEventListener.call(target, type, fn, options); + return () => nativeRemoveEventListener.call(target, type, fn, options); } // https://github.com/rrweb-io/rrweb/pull/407 @@ -70,7 +89,7 @@ export function throttle( wait: number, options: throttleOptions = {}, ) { - let timeout: ReturnType | null = null; + let timeout: ReturnType | number | null = null; let previous = 0; return function (...args: T[]) { const now = Date.now(); @@ -88,7 +107,7 @@ export function throttle( previous = now; func.apply(context, args); } else if (!timeout && options.trailing !== false) { - timeout = setTimeout(() => { + timeout = nativeSetTimeout(() => { previous = options.leading === false ? 0 : Date.now(); timeout = null; func.apply(context, args); @@ -113,7 +132,7 @@ export function hookSetter( : { set(value) { // put hooked setter into event loop to avoid of set latency - setTimeout(() => { + nativeSetTimeout(() => { d.set!.call(this, value); }, 0); if (original && original.set) { diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 0476ce2d68..09f0eec553 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -3,6 +3,7 @@ import type { Mirror, INode, DataURLOptions, + IWindow, } from 'rrweb-snapshot'; export enum EventType { @@ -688,8 +689,6 @@ declare global { } } -export type IWindow = Window & typeof globalThis; - export type Optional = Pick, K> & Omit; export type GetTypedKeys = TakeTypeHelper< @@ -704,3 +703,5 @@ export type TakeTypedKeyValues = Pick< Obj, TakeTypeHelper[keyof TakeTypeHelper] >; + +export { IWindow };