From dae99fd57612589c70e5f5f61565e4d80402467c Mon Sep 17 00:00:00 2001 From: Meg Boehlert Date: Thu, 23 Oct 2025 12:11:19 -0400 Subject: [PATCH 1/2] fix: clear mutation buffer and remove mirror node on iframe pagehide event --- .changeset/eleven-knives-rescue.md | 5 +++++ packages/rrweb/src/record/iframe-manager.ts | 10 ++++++++++ packages/rrweb/src/record/index.ts | 18 ++++++++++++++++-- packages/rrweb/src/record/mutation.ts | 4 ++++ packages/rrweb/src/record/observer.ts | 10 ++++++++++ 5 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 .changeset/eleven-knives-rescue.md 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 1308c378a6..88feb3bc6f 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -4,7 +4,7 @@ import { type SlimDOMOptions, createMirror, } from 'rrweb-snapshot'; -import { initObservers, mutationBuffers } from './observer'; +import { initObservers, mutationBuffers, findAndRemoveIframeBuffer } from './observer'; import { on, getWindowWidth, @@ -437,6 +437,10 @@ function record( try { const handlers: listenerHandler[] = []; + const iframeHandlersMap = new Map< + HTMLIFrameElement, + listenerHandler + >(); const observe = (doc: Document) => { return callbackWrapper(initObservers)( @@ -575,13 +579,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)); @@ -618,6 +631,7 @@ function record( } return () => { handlers.forEach((h) => h()); + iframeHandlersMap.forEach((h) => h()); processedNodeManager.destroy(); recording = false; unregisterErrorHandler(); 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({ From 66f882a1627d938c7abea482bc7704017996d466 Mon Sep 17 00:00:00 2001 From: megboehlert Date: Mon, 27 Oct 2025 20:12:04 +0000 Subject: [PATCH 2/2] Apply formatting changes --- packages/rrweb/src/record/index.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 88feb3bc6f..d87709b8b4 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, findAndRemoveIframeBuffer } from './observer'; +import { + initObservers, + mutationBuffers, + findAndRemoveIframeBuffer, +} from './observer'; import { on, getWindowWidth, @@ -437,10 +441,7 @@ function record( try { const handlers: listenerHandler[] = []; - const iframeHandlersMap = new Map< - HTMLIFrameElement, - listenerHandler - >(); + const iframeHandlersMap = new Map(); const observe = (doc: Document) => { return callbackWrapper(initObservers)(