diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 04d2887050..74a55c2c89 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -442,6 +442,15 @@ interface CacheableImplementations { const cachedImplementations: Partial = {}; +function isNativeFunction(func: unknown): boolean { + return ( + typeof func === 'function' && + /^function\s+\w+\(\)\s+\{\s+\[native code\]\s+\}$/.test( + func.toString(), + ) + ); +} + function getImplementation( name: T, ): CacheableImplementations[T] { @@ -450,8 +459,18 @@ function getImplementation( return cached; } + const impl = window[name] as CacheableImplementations[T]; + + // Fast path to avoid DOM I/O — matches the approach used in + // @sentry-internal/browser-utils getNativeImplementation. + if (isNativeFunction(impl)) { + return (cachedImplementations[name] = impl.bind( + window, + ) as CacheableImplementations[T]); + } + + let sandboxImpl = impl; const document = window.document; - let impl = window[name] as CacheableImplementations[T]; if (document && typeof document.createElement === 'function') { try { const sandbox = document.createElement('iframe'); @@ -459,7 +478,7 @@ function getImplementation( document.head.appendChild(sandbox); const contentWindow = sandbox.contentWindow; if (contentWindow && contentWindow[name]) { - impl = + sandboxImpl = // eslint-disable-next-line @typescript-eslint/unbound-method contentWindow[name] as CacheableImplementations[T]; } @@ -469,7 +488,7 @@ function getImplementation( } } - return (cachedImplementations[name] = impl.bind( + return (cachedImplementations[name] = sandboxImpl.bind( window, ) as CacheableImplementations[T]); } diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 58771a32f6..e1f56010db 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -627,6 +627,15 @@ interface CacheableImplementations { const cachedImplementations: Partial = {}; +function isNativeFunction(func: unknown): boolean { + return ( + typeof func === 'function' && + /^function\s+\w+\(\)\s+\{\s+\[native code\]\s+\}$/.test( + func.toString(), + ) + ); +} + function getImplementation( name: T, ): CacheableImplementations[T] { @@ -635,8 +644,18 @@ function getImplementation( return cached; } + const impl = window[name] as CacheableImplementations[T]; + + // Fast path to avoid DOM I/O — matches the approach used in + // @sentry-internal/browser-utils getNativeImplementation. + if (isNativeFunction(impl)) { + return (cachedImplementations[name] = impl.bind( + window, + ) as CacheableImplementations[T]); + } + + let sandboxImpl = impl; const document = window.document; - let impl = window[name] as CacheableImplementations[T]; if (document && typeof document.createElement === 'function') { try { const sandbox = document.createElement('iframe'); @@ -644,7 +663,7 @@ function getImplementation( document.head.appendChild(sandbox); const contentWindow = sandbox.contentWindow; if (contentWindow && contentWindow[name]) { - impl = + sandboxImpl = // eslint-disable-next-line @typescript-eslint/unbound-method contentWindow[name] as CacheableImplementations[T]; } @@ -654,7 +673,7 @@ function getImplementation( } } - return (cachedImplementations[name] = impl.bind( + return (cachedImplementations[name] = sandboxImpl.bind( window, ) as CacheableImplementations[T]); }