Browse Source

fix(replay): Cache replay dom nodes using a simple cache key (#52032)

Before we weren't caching the results of `useExtractedDomNodes`. So the
expensive work was being re-done on every re-render. Also, notice
how`hashQueryKey` was taking forever. This is because it was calling
`JSON.stringify` on our entire ReplayReader, not good.

Now we've implemented toJSON which makes hasQueryKey simple, and we've
set the cache time to forever. Cache will be reset if there's a new
ReplayReader instance.

See below for a flame graph where i'm recording scrolling down the DOM
nodes tab. There's about 3 pages of dom nodes to get through. Before we
can clearly see that useExtractedDomNodes was called multiple times, and
takes a while each time.
After it's smooth, rendering updates as we go.

| Before | After |
| --- | --- |
| <img width="1515" alt="SCR-20230630-jzwx"
src="https://github.com/getsentry/sentry/assets/187460/dd59f402-9687-4ccd-be0d-2099138f5cb6">
| <img width="1415" alt="SCR-20230630-jzrc"
src="https://github.com/getsentry/sentry/assets/187460/a5cf15da-af94-47d0-b5cd-69c0896d87be">
|


Fixes https://github.com/getsentry/team-replay/issues/105
Ryan Albrecht 1 year ago
parent
commit
7be48fd2b5

+ 6 - 0
static/app/utils/replays/replayReader.tsx

@@ -4,6 +4,7 @@ import {duration} from 'moment';
 
 import type {Crumb} from 'sentry/types/breadcrumbs';
 import {BreadcrumbType} from 'sentry/types/breadcrumbs';
+import domId from 'sentry/utils/domId';
 import localStorageWrapper from 'sentry/utils/localStorage';
 import extractDomNodes from 'sentry/utils/replays/extractDomNodes';
 import hydrateBreadcrumbs, {
@@ -100,6 +101,8 @@ export default class ReplayReader {
     errors,
     replayRecord,
   }: RequiredNotNull<ReplayReaderParams>) {
+    this._cacheKey = domId('replayReader-');
+
     const {breadcrumbFrames, optionFrame, rrwebFrames, spanFrames} =
       hydrateFrames(attachments);
 
@@ -190,6 +193,7 @@ export default class ReplayReader {
 
   public timestampDeltas = {startedAtDelta: 0, finishedAtDelta: 0};
 
+  private _cacheKey: string;
   private _errors: ErrorFrame[];
   private _optionFrame: undefined | OptionFrame;
   private _sortedBreadcrumbFrames: BreadcrumbFrame[];
@@ -202,6 +206,8 @@ export default class ReplayReader {
   private rrwebEvents: RecordingEvent[];
   private breadcrumbs: Crumb[];
 
+  toJSON = () => this._cacheKey;
+
   /**
    * @returns Duration of Replay (milliseonds)
    */

+ 1 - 0
static/app/views/replays/detail/domMutations/index.tsx

@@ -35,6 +35,7 @@ function useExtractedDomNodes({replay}: {replay: null | ReplayReader}) {
   return useQuery(['getDomNodes', replay], () => replay?.getDomNodes() ?? [], {
     enabled: Boolean(replay),
     initialData: [],
+    cacheTime: Infinity,
   });
 }