diff --git a/.changeset/eleven-knives-rescue.md b/.changeset/eleven-knives-rescue.md new file mode 100644 index 0000000000..d9ff3d35e9 --- /dev/null +++ b/.changeset/eleven-knives-rescue.md @@ -0,0 +1,5 @@ +--- +"rrweb": patch +--- + +To prevent mem leak, reset and clear mutation buffers and remove iframe node from mirror on iframes pagehide event diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 0451743619..117319d8e7 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -23,6 +23,7 @@ export class IframeManager { private mutationCb: mutationCallBack; private wrappedEmit: (e: eventWithoutTime, isCheckout?: boolean) => void; private loadListener?: (iframeEl: HTMLIFrameElement) => unknown; + private pageHideListener?: (iframeEl: HTMLIFrameElement) => unknown; private stylesheetManager: StylesheetManager; private recordCrossOriginIframes: boolean; @@ -58,6 +59,10 @@ export class IframeManager { this.loadListener = cb; } + public addPageHideListener(cb: (iframeEl: HTMLIFrameElement) => unknown) { + this.pageHideListener = cb; + } + public attachIframe( iframeEl: HTMLIFrameElement, childSn: serializedNodeWithId, @@ -83,6 +88,11 @@ export class IframeManager { this.handleMessage.bind(this), ); + iframeEl.contentWindow?.addEventListener('pagehide', () => { + this.pageHideListener?.(iframeEl); + this.mirror.removeNodeFromMap(iframeEl.contentDocument!); + this.crossOriginIframeMap.delete(iframeEl.contentWindow!); + }); this.loadListener?.(iframeEl); if ( diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 65da8ec801..b0786f2c18 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -4,7 +4,11 @@ import { type SlimDOMOptions, createMirror, } from 'rrweb-snapshot'; -import { initObservers, mutationBuffers } from './observer'; +import { + initObservers, + mutationBuffers, + findAndRemoveIframeBuffer, +} from './observer'; import { on, getWindowWidth, @@ -437,6 +441,7 @@ function record( try { const handlers: listenerHandler[] = []; + const iframeHandlersMap = new Map(); const observe = (doc: Document) => { return callbackWrapper(initObservers)( @@ -575,13 +580,22 @@ function record( iframeManager.addLoadListener((iframeEl) => { try { - handlers.push(observe(iframeEl.contentDocument!)); + iframeHandlersMap.set(iframeEl, observe(iframeEl.contentDocument!)); } catch (error) { // TODO: handle internal error console.warn(error); } }); + iframeManager.addPageHideListener((iframeEl) => { + const iframeHandler = iframeHandlersMap.get(iframeEl); + if (iframeHandler) { + iframeHandler(); + iframeHandlersMap.delete(iframeEl); + } + findAndRemoveIframeBuffer(iframeEl); + }); + const init = () => { takeFullSnapshot(); handlers.push(observe(document)); @@ -617,6 +631,7 @@ function record( ); } return () => { + iframeHandlersMap.forEach((h) => h()); handlers.forEach((handler) => { try { handler(); diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 08e927a98f..7d59d5062b 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -521,6 +521,10 @@ export default class MutationBuffer { this.mutationCb(payload); }; + public bufferBelongsToIframe = (iframeEl: HTMLIFrameElement) => { + return this.doc === iframeEl.contentDocument; + }; + private genTextAreaValueMutation = (textarea: HTMLTextAreaElement) => { let item = this.attributeMap.get(textarea); if (!item) { diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 8326d79651..ea41744759 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -376,6 +376,16 @@ function initViewportResizeObserver( return on('resize', updateDimension, win); } +export function findAndRemoveIframeBuffer(iframeEl: HTMLIFrameElement) { + for (let i = mutationBuffers.length - 1; i >= 0; i--) { + const buf = mutationBuffers[i]; + if (buf.bufferBelongsToIframe(iframeEl)) { + buf.reset(); + mutationBuffers.splice(i, 1); + } + } +} + export const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT']; const lastInputValueMap: WeakMap = new WeakMap(); function initInputObserver({