From 92c99b6efb87ec106d32744446a666b71b641b26 Mon Sep 17 00:00:00 2001
From: Eoghan Murray <eoghan@getthere.ie>
Date: Tue, 9 Apr 2024 12:49:32 +0100
Subject: [PATCH] See #1339 - perf: remove a nested function call and an object
 clone per event

---
 .changeset/event-single-wrap.md             |   5 +
 packages/rrweb/src/record/iframe-manager.ts |  10 +-
 packages/rrweb/src/record/index.ts          | 299 +++++++++-----------
 packages/rrweb/test/utils.ts                |   4 +-
 packages/types/src/index.ts                 |   4 +-
 5 files changed, 145 insertions(+), 177 deletions(-)
 create mode 100644 .changeset/event-single-wrap.md

diff --git a/.changeset/event-single-wrap.md b/.changeset/event-single-wrap.md
new file mode 100644
index 0000000000..c659c015ef
--- /dev/null
+++ b/.changeset/event-single-wrap.md
@@ -0,0 +1,5 @@
+---
+'rrweb': patch
+---
+
+perf: Avoid an extra function call and object clone during event emission
diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts
index 26985cc49a..2d9205c0cb 100644
--- a/packages/rrweb/src/record/iframe-manager.ts
+++ b/packages/rrweb/src/record/iframe-manager.ts
@@ -3,7 +3,11 @@ import { genId, NodeType } from 'rrweb-snapshot';
 import type { CrossOriginIframeMessageEvent } from '../types';
 import CrossOriginIframeMirror from './cross-origin-iframe-mirror';
 import { EventType, IncrementalSource } from '@rrweb/types';
-import type { eventWithTime, mutationCallBack } from '@rrweb/types';
+import type {
+  eventWithTime,
+  eventWithoutTime,
+  mutationCallBack,
+} from '@rrweb/types';
 import type { StylesheetManager } from './stylesheet-manager';
 
 export class IframeManager {
@@ -16,7 +20,7 @@ export class IframeManager {
     new WeakMap();
   private mirror: Mirror;
   private mutationCb: mutationCallBack;
-  private wrappedEmit: (e: eventWithTime, isCheckout?: boolean) => void;
+  private wrappedEmit: (e: eventWithoutTime, isCheckout?: boolean) => void;
   private loadListener?: (iframeEl: HTMLIFrameElement) => unknown;
   private stylesheetManager: StylesheetManager;
   private recordCrossOriginIframes: boolean;
@@ -26,7 +30,7 @@ export class IframeManager {
     mutationCb: mutationCallBack;
     stylesheetManager: StylesheetManager;
     recordCrossOriginIframes: boolean;
-    wrappedEmit: (e: eventWithTime, isCheckout?: boolean) => void;
+    wrappedEmit: (e: eventWithoutTime, isCheckout?: boolean) => void;
   }) {
     this.mutationCb = options.mutationCb;
     this.wrappedEmit = options.wrappedEmit;
diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts
index 3b4475cfc9..7be978199d 100644
--- a/packages/rrweb/src/record/index.ts
+++ b/packages/rrweb/src/record/index.ts
@@ -19,7 +19,7 @@ import {
 import type { recordOptions } from '../types';
 import {
   EventType,
-  event,
+  eventWithoutTime,
   eventWithTime,
   IncrementalSource,
   listenerHandler,
@@ -40,14 +40,7 @@ import {
   unregisterErrorHandler,
 } from './error-handler';
 
-function wrapEvent(e: event): eventWithTime {
-  return {
-    ...e,
-    timestamp: nowTimestamp(),
-  };
-}
-
-let wrappedEmit!: (e: eventWithTime, isCheckout?: boolean) => void;
+let wrappedEmit!: (e: eventWithoutTime, isCheckout?: boolean) => void;
 
 let takeFullSnapshot!: (isCheckout?: boolean) => void;
 let canvasManager!: CanvasManager;
@@ -187,7 +180,9 @@ function record<T = eventWithTime>(
     }
     return e as unknown as T;
   };
-  wrappedEmit = (e: eventWithTime, isCheckout?: boolean) => {
+  wrappedEmit = (r: eventWithoutTime, isCheckout?: boolean) => {
+    const e = r as eventWithTime;
+    e.timestamp = nowTimestamp();
     if (
       mutationBuffers[0]?.isFrozen() &&
       e.type !== EventType.FullSnapshot &&
@@ -238,47 +233,39 @@ function record<T = eventWithTime>(
   };
 
   const wrappedMutationEmit = (m: mutationCallbackParam) => {
-    wrappedEmit(
-      wrapEvent({
-        type: EventType.IncrementalSnapshot,
-        data: {
-          source: IncrementalSource.Mutation,
-          ...m,
-        },
-      }),
-    );
+    wrappedEmit({
+      type: EventType.IncrementalSnapshot,
+      data: {
+        source: IncrementalSource.Mutation,
+        ...m,
+      },
+    });
   };
   const wrappedScrollEmit: scrollCallback = (p) =>
-    wrappedEmit(
-      wrapEvent({
-        type: EventType.IncrementalSnapshot,
-        data: {
-          source: IncrementalSource.Scroll,
-          ...p,
-        },
-      }),
-    );
+    wrappedEmit({
+      type: EventType.IncrementalSnapshot,
+      data: {
+        source: IncrementalSource.Scroll,
+        ...p,
+      },
+    });
   const wrappedCanvasMutationEmit = (p: canvasMutationParam) =>
-    wrappedEmit(
-      wrapEvent({
-        type: EventType.IncrementalSnapshot,
-        data: {
-          source: IncrementalSource.CanvasMutation,
-          ...p,
-        },
-      }),
-    );
+    wrappedEmit({
+      type: EventType.IncrementalSnapshot,
+      data: {
+        source: IncrementalSource.CanvasMutation,
+        ...p,
+      },
+    });
 
   const wrappedAdoptedStyleSheetEmit = (a: adoptedStyleSheetParam) =>
-    wrappedEmit(
-      wrapEvent({
-        type: EventType.IncrementalSnapshot,
-        data: {
-          source: IncrementalSource.AdoptedStyleSheet,
-          ...a,
-        },
-      }),
-    );
+    wrappedEmit({
+      type: EventType.IncrementalSnapshot,
+      data: {
+        source: IncrementalSource.AdoptedStyleSheet,
+        ...a,
+      },
+    });
 
   const stylesheetManager = new StylesheetManager({
     mutationCb: wrappedMutationEmit,
@@ -350,14 +337,14 @@ function record<T = eventWithTime>(
       return;
     }
     wrappedEmit(
-      wrapEvent({
+      {
         type: EventType.Meta,
         data: {
           href: window.location.href,
           width: getWindowWidth(),
           height: getWindowHeight(),
         },
-      }),
+      },
       isCheckout,
     );
 
@@ -406,13 +393,13 @@ function record<T = eventWithTime>(
     }
 
     wrappedEmit(
-      wrapEvent({
+      {
         type: EventType.FullSnapshot,
         data: {
           node,
           initialOffset: getWindowScroll(window),
         },
-      }),
+      },
       isCheckout,
     );
     mutationBuffers.forEach((buf) => buf.unlock()); // generate & emit any mutations that happened during snapshotting, as can now apply against the newly built mirror
@@ -433,108 +420,88 @@ function record<T = eventWithTime>(
         {
           mutationCb: wrappedMutationEmit,
           mousemoveCb: (positions, source) =>
-            wrappedEmit(
-              wrapEvent({
-                type: EventType.IncrementalSnapshot,
-                data: {
-                  source,
-                  positions,
-                },
-              }),
-            ),
+            wrappedEmit({
+              type: EventType.IncrementalSnapshot,
+              data: {
+                source,
+                positions,
+              },
+            }),
           mouseInteractionCb: (d) =>
-            wrappedEmit(
-              wrapEvent({
-                type: EventType.IncrementalSnapshot,
-                data: {
-                  source: IncrementalSource.MouseInteraction,
-                  ...d,
-                },
-              }),
-            ),
+            wrappedEmit({
+              type: EventType.IncrementalSnapshot,
+              data: {
+                source: IncrementalSource.MouseInteraction,
+                ...d,
+              },
+            }),
           scrollCb: wrappedScrollEmit,
           viewportResizeCb: (d) =>
-            wrappedEmit(
-              wrapEvent({
-                type: EventType.IncrementalSnapshot,
-                data: {
-                  source: IncrementalSource.ViewportResize,
-                  ...d,
-                },
-              }),
-            ),
+            wrappedEmit({
+              type: EventType.IncrementalSnapshot,
+              data: {
+                source: IncrementalSource.ViewportResize,
+                ...d,
+              },
+            }),
           inputCb: (v) =>
-            wrappedEmit(
-              wrapEvent({
-                type: EventType.IncrementalSnapshot,
-                data: {
-                  source: IncrementalSource.Input,
-                  ...v,
-                },
-              }),
-            ),
+            wrappedEmit({
+              type: EventType.IncrementalSnapshot,
+              data: {
+                source: IncrementalSource.Input,
+                ...v,
+              },
+            }),
           mediaInteractionCb: (p) =>
-            wrappedEmit(
-              wrapEvent({
-                type: EventType.IncrementalSnapshot,
-                data: {
-                  source: IncrementalSource.MediaInteraction,
-                  ...p,
-                },
-              }),
-            ),
+            wrappedEmit({
+              type: EventType.IncrementalSnapshot,
+              data: {
+                source: IncrementalSource.MediaInteraction,
+                ...p,
+              },
+            }),
           styleSheetRuleCb: (r) =>
-            wrappedEmit(
-              wrapEvent({
-                type: EventType.IncrementalSnapshot,
-                data: {
-                  source: IncrementalSource.StyleSheetRule,
-                  ...r,
-                },
-              }),
-            ),
+            wrappedEmit({
+              type: EventType.IncrementalSnapshot,
+              data: {
+                source: IncrementalSource.StyleSheetRule,
+                ...r,
+              },
+            }),
           styleDeclarationCb: (r) =>
-            wrappedEmit(
-              wrapEvent({
-                type: EventType.IncrementalSnapshot,
-                data: {
-                  source: IncrementalSource.StyleDeclaration,
-                  ...r,
-                },
-              }),
-            ),
+            wrappedEmit({
+              type: EventType.IncrementalSnapshot,
+              data: {
+                source: IncrementalSource.StyleDeclaration,
+                ...r,
+              },
+            }),
           canvasMutationCb: wrappedCanvasMutationEmit,
           fontCb: (p) =>
-            wrappedEmit(
-              wrapEvent({
-                type: EventType.IncrementalSnapshot,
-                data: {
-                  source: IncrementalSource.Font,
-                  ...p,
-                },
-              }),
-            ),
+            wrappedEmit({
+              type: EventType.IncrementalSnapshot,
+              data: {
+                source: IncrementalSource.Font,
+                ...p,
+              },
+            }),
           selectionCb: (p) => {
-            wrappedEmit(
-              wrapEvent({
-                type: EventType.IncrementalSnapshot,
-                data: {
-                  source: IncrementalSource.Selection,
-                  ...p,
-                },
-              }),
-            );
+            wrappedEmit({
+              type: EventType.IncrementalSnapshot,
+              data: {
+                source: IncrementalSource.Selection,
+                ...p,
+              },
+            });
           },
           customElementCb: (c) => {
-            wrappedEmit(
-              wrapEvent({
-                type: EventType.IncrementalSnapshot,
-                data: {
-                  source: IncrementalSource.CustomElement,
-                  ...c,
-                },
-              }),
-            );
+            wrappedEmit({
+              type: EventType.IncrementalSnapshot,
+              data: {
+                source: IncrementalSource.CustomElement,
+                ...c,
+              },
+            });
           },
           blockClass,
           ignoreClass,
@@ -570,15 +537,13 @@ function record<T = eventWithTime>(
                 observer: p.observer!,
                 options: p.options,
                 callback: (payload: object) =>
-                  wrappedEmit(
-                    wrapEvent({
-                      type: EventType.Plugin,
-                      data: {
-                        plugin: p.name,
-                        payload,
-                      },
-                    }),
-                  ),
+                  wrappedEmit({
+                    type: EventType.Plugin,
+                    data: {
+                      plugin: p.name,
+                      payload,
+                    },
+                  }),
               })) || [],
         },
         hooks,
@@ -607,12 +572,10 @@ function record<T = eventWithTime>(
     } else {
       handlers.push(
         on('DOMContentLoaded', () => {
-          wrappedEmit(
-            wrapEvent({
-              type: EventType.DomContentLoaded,
-              data: {},
-            }),
-          );
+          wrappedEmit({
+            type: EventType.DomContentLoaded,
+            data: {},
+          });
           if (recordAfter === 'DOMContentLoaded') init();
         }),
       );
@@ -620,12 +583,10 @@ function record<T = eventWithTime>(
         on(
           'load',
           () => {
-            wrappedEmit(
-              wrapEvent({
-                type: EventType.Load,
-                data: {},
-              }),
-            );
+            wrappedEmit({
+              type: EventType.Load,
+              data: {},
+            });
             if (recordAfter === 'load') init();
           },
           window,
@@ -648,15 +609,13 @@ record.addCustomEvent = <T>(tag: string, payload: T) => {
   if (!recording) {
     throw new Error('please add custom event after start recording');
   }
-  wrappedEmit(
-    wrapEvent({
-      type: EventType.Custom,
-      data: {
-        tag,
-        payload,
-      },
-    }),
-  );
+  wrappedEmit({
+    type: EventType.Custom,
+    data: {
+      tag,
+      payload,
+    },
+  });
 };
 
 record.freezePage = () => {
diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts
index 6cd93281f9..4905ca5452 100644
--- a/packages/rrweb/test/utils.ts
+++ b/packages/rrweb/test/utils.ts
@@ -3,10 +3,10 @@ import {
   EventType,
   IncrementalSource,
   eventWithTime,
+  eventWithoutTime,
   MouseInteractions,
   Optional,
   mouseInteractionData,
-  event,
   pluginEvent,
 } from '@rrweb/types';
 import type { recordOptions } from '../src/types';
@@ -226,7 +226,7 @@ function stringifySnapshots(snapshots: eventWithTime[]): string {
           }
         }
         delete (s as Optional<eventWithTime, 'timestamp'>).timestamp;
-        return s as event;
+        return s as eventWithoutTime;
       }),
     null,
     2,
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index c41d3e97ff..cc00c55f1a 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -163,7 +163,7 @@ export type incrementalData =
   | adoptedStyleSheetData
   | customElementData;
 
-export type event =
+export type eventWithoutTime =
   | domContentLoadedEvent
   | loadedEvent
   | fullSnapshotEvent
@@ -172,7 +172,7 @@ export type event =
   | customEvent
   | pluginEvent;
 
-export type eventWithTime = event & {
+export type eventWithTime = eventWithoutTime & {
   timestamp: number;
   delay?: number;
 };