Skip to content

improve replay perf#935

Merged
goldflag merged 3 commits intomasterfrom
session-replay-perf
Mar 12, 2026
Merged

improve replay perf#935
goldflag merged 3 commits intomasterfrom
session-replay-perf

Conversation

@goldflag
Copy link
Collaborator

@goldflag goldflag commented Mar 12, 2026

Summary by CodeRabbit

  • Performance Improvements

    • Virtualized replay breadcrumbs for smoother scrolling with many events.
    • Reduced re-renders across player, controls, timeline, and lists via selective state subscriptions.
    • Dynamic player resizing to update dimensions without recreating the player.
  • New Features

    • Grouped breadcrumb interactions (jump to group middle) and improved event seeking behavior.
    • Group descriptions now show counts when grouping occurs.
  • Refactor

    • Memoized controls and handlers for more stable UI updates.

@vercel
Copy link

vercel bot commented Mar 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
rybbit Ready Ready Preview, Comment Mar 12, 2026 6:22pm

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 12, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 36ae7029-1f24-4558-a70c-b00f4e402b14

📥 Commits

Reviewing files that changed from the base of the PR and between 587b96f and a621192.

📒 Files selected for processing (2)
  • client/src/components/replay/ReplayBreadcrumbs.tsx
  • client/src/components/replay/player/ReplayPlayerTopbar.tsx

📝 Walkthrough

Walkthrough

Added virtualization to replay breadcrumbs; switched many replay/globe components to shallow Zustand selectors; memoized callbacks and wrapped controls with memo; refactored rrweb player hook to use refs/playerRef for dimension updates and removed setActivityPeriods from the hook's store destructuring.

Changes

Cohort / File(s) Summary
Breadcrumbs Virtualization
client/src/components/replay/ReplayBreadcrumbs.tsx
Replaced direct mapping with a scroll-based virtualizer (useVirtualizer); introduced EventGroup model, grouping helpers, scroll container, measurement refs, absolute-positioned virtual items, and group click → seek handlers.
Player UI & Controls
client/src/components/replay/player/ReplayPlayer.tsx, client/src/components/replay/player/ReplayPlayerControls.tsx, client/src/components/replay/player/ReplayPlayerTopbar.tsx
Switched components to shallow-selected store reads; memoized event handlers with useCallback; wrapped controls in memo; moved getDisplayPath out of component and adjusted page-view filtering to numeric comparison.
Player Hook (rrweb) Updates
client/src/components/replay/player/hooks/useReplayPlayer.ts
Replaced width/height props with refs and added playerRef to retain and update rrweb-player instance without recreating; update dimensions via player instance and triggerResize; removed setActivityPeriods from destructured store fields.
Globe / Timeline Store Selection
client/src/app/[site]/globe/3d/hooks/timelineLayer/useTimelineSessions.ts, client/src/app/[site]/globe/components/TimelineScrubber.tsx, client/src/app/[site]/globe/page.tsx
Applied useShallow selectors to timeline/globe stores to subscribe only to needed fields (reducing re-render scope).
Replay List / Card Store Selection
client/src/app/[site]/replay/components/ReplayCard.tsx, client/src/app/[site]/replay/components/ReplayList.tsx
Replaced full store reads with shallow-selected subsets (e.g., sessionId, setSessionId, minDuration) to limit re-render triggers.
Other
package.json
Implied dependency changes for virtualization (e.g., @tanstack/react-virtual) referenced by diffs.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Breadcrumbs as "ReplayBreadcrumbs"
  participant Store as "ReplayStore"
  participant PlayerComp as "ReplayPlayer (component)"
  participant RRWeb as "rrweb-player"

  User->>Breadcrumbs: click event or group click (time)
  Breadcrumbs->>Store: setCurrentTime(time)
  Breadcrumbs->>PlayerComp: request goto(time)
  PlayerComp->>RRWeb: rrweb.player.goto(time)
  RRWeb-->>PlayerComp: playback position updated
  PlayerComp->>Store: setCurrentTime(updated)
  Store-->>Breadcrumbs: currentTime change (shallow subs)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hop through virtual crumbs that barely show,
I tuck each grouped moment where the seekers go,
With refs I hold the player’s frame just so,
Memo'd and shallow, I dance light and low,
Replay the trail — a tidy bunny show!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'improve replay perf' is vague and lacks specificity. While it references replay performance, it does not clearly describe the actual implementation approach (virtualization, memoization, shallow selectors) or the specific areas optimized. Consider a more descriptive title such as 'optimize replay performance with virtualization and memoization' that better conveys the primary changes and techniques used.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch session-replay-perf
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…ow state selection with Zustand for improved performance and reactivity. Update multiple components including GlobePage, TimelineScrubber, ReplayCard, and others to enhance data handling efficiency.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/src/components/replay/player/ReplayPlayerTopbar.tsx`:
- Around line 45-51: The filter is comparing event.type to the number 4 but
upstream events may have type as a string, so normalize/coerce event.type before
the comparison in ReplayPlayerTopbar (the loop over data.events that builds
transitions using firstTimestamp and pushes into transitions). Update the check
to coerce event.type (e.g., Number(event.type) or parseInt(event.type as string,
10)) and then compare to 4, keeping the existing event.data?.href guard so
pageTransitions are populated correctly.

In `@client/src/components/replay/ReplayBreadcrumbs.tsx`:
- Around line 243-253: handleEventClick is writing seconds into the shared
currentTime store while player.goto, slider math, and topbar transitions expect
milliseconds; change the setCurrentTime call to use milliseconds (timeInMs)
instead of timeInSeconds so the shared currentTime stays in ms. Locate
handleEventClick (uses firstTimestamp, player.goto, setCurrentTime) and replace
setCurrentTime(timeInSeconds) with setCurrentTime(timeInMs) so all consumers
remain consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 698e78eb-9244-4f5a-a561-9688ba9fed0c

📥 Commits

Reviewing files that changed from the base of the PR and between 3674af1 and 1304531.

📒 Files selected for processing (5)
  • client/src/components/replay/ReplayBreadcrumbs.tsx
  • client/src/components/replay/player/ReplayPlayer.tsx
  • client/src/components/replay/player/ReplayPlayerControls.tsx
  • client/src/components/replay/player/ReplayPlayerTopbar.tsx
  • client/src/components/replay/player/hooks/useReplayPlayer.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
client/src/components/replay/player/ReplayPlayerTopbar.tsx (1)

48-55: ⚠️ Potential issue | 🟠 Major

Coerce event.type before comparing to avoid type mismatch.

Line 49 compares event.type === 4 using strict equality with a number. However, based on the codebase patterns (see ReplayBreadcrumbs.tsx which uses String(event.type) before comparing to "4"), event types may arrive as strings from the server. This would leave pageTransitions empty and break the URL tracking.

🔧 Suggested fix
     for (const event of data.events) {
-      if (event.type === 4 && event.data?.href) {
+      if (Number(event.type) === 4 && event.data?.href) {
         transitions.push({
           time: event.timestamp - firstTimestamp,
           url: event.data.href,
         });
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/replay/player/ReplayPlayerTopbar.tsx` around lines 48 -
55, The loop over data.events checks event.type with strict numeric equality
which fails if the server sends it as a string; update the comparison in the
loop that builds transitions (the block that iterates over data.events and
pushes into transitions using firstTimestamp, event.timestamp and
event.data?.href) to coerce event.type before comparing (e.g., use
String(event.type) === "4" or Number(event.type) === 4) so page transitions are
detected reliably regardless of incoming type.
🧹 Nitpick comments (6)
client/src/app/[site]/globe/3d/hooks/timelineLayer/useTimelineSessions.ts (1)

9-9: Consider moving this import to the external imports group.

Per coding guidelines, external package imports should be grouped before internal imports. Move this import after line 3 (with the other external imports from @tanstack/react-query, luxon, and react).

📦 Suggested import reordering
 import { useQuery } from "@tanstack/react-query";
 import { DateTime } from "luxon";
 import { useEffect, useMemo } from "react";
+import { useShallow } from "zustand/react/shallow";
 import { fetchSessions, GetSessionsResponse, SessionsParams } from "../../../../../../api/analytics/endpoints";
 import { APIResponse } from "../../../../../../api/types";
 import { authedFetch, buildApiParams } from "../../../../../../api/utils";
 import { getFilteredFilters, useStore } from "../../../../../../lib/store";
 import { SESSION_PAGE_FILTERS } from "../../../../../../lib/filterGroups";
-import { useShallow } from "zustand/react/shallow";
 import { useTimelineStore } from "../../../timelineStore";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/`[site]/globe/3d/hooks/timelineLayer/useTimelineSessions.ts at
line 9, The import "import { useShallow } from 'zustand/react/shallow'" is
currently in the internal imports group; move it into the external imports group
alongside other third-party imports (e.g., the `@tanstack/react-query`, luxon, and
react imports) so external packages are grouped first, keeping import ordering
consistent with project guidelines and keeping the import line "useShallow"
adjacent to other external dependencies.
client/src/app/[site]/globe/page.tsx (1)

26-26: Move useShallow into the external import block.

Line 26 places a third-party import after internal relative imports, so the file no longer follows the repo’s import grouping rule. As per coding guidelines, "Organize imports with external packages first, then internal imports; sort alphabetically within groups."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/`[site]/globe/page.tsx at line 26, The import of the
third-party hook useShallow should be moved into the external imports block at
the top of client/src/app/[site]/globe/page.tsx so external packages are grouped
before internal relative imports; locate the current import statement "import {
useShallow } from 'zustand/react/shallow';" and relocate it into the external
imports section, maintaining alphabetical order among external imports and
preserving existing internal relative imports order.
client/src/components/replay/player/ReplayPlayerTopbar.tsx (1)

1-12: Import order inconsistency.

useShallow from zustand/react/shallow (line 11) is an external package import but appears after internal imports. As per coding guidelines, organize imports with external packages first, then internal imports.

♻️ Suggested reordering
 import Link from "next/link";
 import { useParams } from "next/navigation";
 import { useMemo } from "react";
+import { useShallow } from "zustand/react/shallow";
 import { useGetSessionReplayEvents } from "@/api/analytics/hooks/sessionReplay/useGetSessionReplayEvents";
 import {
   BrowserTooltipIcon,
   CountryFlagTooltipIcon,
   DeviceTypeTooltipIcon,
   OperatingSystemTooltipIcon,
 } from "@/components/TooltipIcons/TooltipIcons";
-import { useShallow } from "zustand/react/shallow";
 import { useReplayStore } from "../replayStore";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/replay/player/ReplayPlayerTopbar.tsx` around lines 1 -
12, Imports are out of order: move external package imports (e.g., Link from
"next/link", useParams from "next/navigation", useMemo from "react", useShallow
from "zustand/react/shallow") above internal project imports (e.g.,
useGetSessionReplayEvents,
BrowserTooltipIcon/CountryFlagTooltipIcon/DeviceTypeTooltipIcon/OperatingSystemTooltipIcon,
useReplayStore) so that external dependencies appear first, then internal
modules; reorder the import block accordingly to satisfy the project's import
ordering guideline.
client/src/app/[site]/replay/components/ReplayList.tsx (1)

9-10: Keep useShallow in the external import group.

zustand/react/shallow is a package import, so it should sit with the other third-party imports rather than below the local modules.

As per coding guidelines, "Organize imports with external packages first, then internal imports; sort alphabetically within groups".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/`[site]/replay/components/ReplayList.tsx around lines 9 - 10,
Move the external import "useShallow" from "zustand/react/shallow" into the top
external-import group above internal imports; specifically, place the line
importing useShallow before the local import of useReplayStore (from
"@/components/replay/replayStore") and ensure imports are alphabetized within
the external and internal groups so "useShallow" sits with other third-party
imports.
client/src/app/[site]/replay/components/ReplayCard.tsx (1)

31-32: Keep useShallow with the external imports.

This adds a package import inside the internal import block. Move it up with the other third-party imports.

As per coding guidelines, "Organize imports with external packages first, then internal imports; sort alphabetically within groups".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/`[site]/replay/components/ReplayCard.tsx around lines 31 - 32,
The import for useShallow is currently placed with internal imports; move the
import statement "useShallow" from "zustand/react/shallow" into the
external/third-party import block above the internal imports (where other
external packages are listed) and ensure external imports are alphabetized; keep
the internal import "useReplayStore" (from "@/components/replay/replayStore") in
the internal group below. This will restore the external-first, alphabetized
import order in ReplayCard.tsx.
client/src/components/replay/player/hooks/useReplayPlayer.ts (1)

15-23: Type the player handle instead of using any.

The new resize path now depends on $set() and triggerResize(), so useRef<any> gives up the compile-time checks this refactor needs most. I’d give the ref a narrow player interface and mirror that type on newPlayer as well.

♻️ Suggested typing shape
+interface ReplayPlayerHandle {
+  addEventListener: (name: string, listener: (event: { payload: unknown }) => void) => void;
+  pause: () => void;
+  play: () => void;
+  getMetaData: () => { totalTime: number; isPlaying?: boolean };
+  $set: (props: { width: number; height: number }) => void;
+  triggerResize: () => void;
+}
+
 export const useReplayPlayer = ({ data, width, height }: UseReplayPlayerProps) => {
   const playerContainerRef = useRef<HTMLDivElement>(null);
-  const playerRef = useRef<any>(null);
+  const playerRef = useRef<ReplayPlayerHandle | null>(null);
Please verify the exact `rrweb-player` instance surface exposed by the installed version before locking the interface. As per coding guidelines, "Use TypeScript with strict typing throughout both client and server".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/replay/player/hooks/useReplayPlayer.ts` around lines 15
- 23, Replace the untyped playerRef and newPlayer uses with a narrow TypeScript
interface describing only the rrweb-player surface your code uses (e.g., methods
like $set, triggerResize, play/pause, getCurrentTime/setCurrentTime, getDuration
or other members referenced in useReplayPlayer). Change playerRef from
useRef<any>(null) to useRef<PlayerHandle | null>(null) and type the newPlayer
variable as PlayerHandle when creating the player, then update any call sites to
respect the nullable ref. Confirm the exact method names/signatures against the
installed rrweb-player version and keep the interface minimal to cover only the
functions used by useReplayPlayer (playerRef, newPlayer, $set, triggerResize,
and any time/control methods).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/src/components/replay/player/hooks/useReplayPlayer.ts`:
- Around line 46-48: Derived player height can go negative when
heightRef.current is 0; compute a clamped height (e.g., max(0, heightRef.current
- CONTROLS_HEIGHT)) and use that value instead of heightRef.current -
CONTROLS_HEIGHT in the player initialization and any subsequent $set() calls.
Update the code paths that read widthRef.current and heightRef.current
(including the initialization and the places referenced by the $set() call) to
use the clampedHeight variable so you never pass a negative height into the
player.

---

Duplicate comments:
In `@client/src/components/replay/player/ReplayPlayerTopbar.tsx`:
- Around line 48-55: The loop over data.events checks event.type with strict
numeric equality which fails if the server sends it as a string; update the
comparison in the loop that builds transitions (the block that iterates over
data.events and pushes into transitions using firstTimestamp, event.timestamp
and event.data?.href) to coerce event.type before comparing (e.g., use
String(event.type) === "4" or Number(event.type) === 4) so page transitions are
detected reliably regardless of incoming type.

---

Nitpick comments:
In `@client/src/app/`[site]/globe/3d/hooks/timelineLayer/useTimelineSessions.ts:
- Line 9: The import "import { useShallow } from 'zustand/react/shallow'" is
currently in the internal imports group; move it into the external imports group
alongside other third-party imports (e.g., the `@tanstack/react-query`, luxon, and
react imports) so external packages are grouped first, keeping import ordering
consistent with project guidelines and keeping the import line "useShallow"
adjacent to other external dependencies.

In `@client/src/app/`[site]/globe/page.tsx:
- Line 26: The import of the third-party hook useShallow should be moved into
the external imports block at the top of client/src/app/[site]/globe/page.tsx so
external packages are grouped before internal relative imports; locate the
current import statement "import { useShallow } from 'zustand/react/shallow';"
and relocate it into the external imports section, maintaining alphabetical
order among external imports and preserving existing internal relative imports
order.

In `@client/src/app/`[site]/replay/components/ReplayCard.tsx:
- Around line 31-32: The import for useShallow is currently placed with internal
imports; move the import statement "useShallow" from "zustand/react/shallow"
into the external/third-party import block above the internal imports (where
other external packages are listed) and ensure external imports are
alphabetized; keep the internal import "useReplayStore" (from
"@/components/replay/replayStore") in the internal group below. This will
restore the external-first, alphabetized import order in ReplayCard.tsx.

In `@client/src/app/`[site]/replay/components/ReplayList.tsx:
- Around line 9-10: Move the external import "useShallow" from
"zustand/react/shallow" into the top external-import group above internal
imports; specifically, place the line importing useShallow before the local
import of useReplayStore (from "@/components/replay/replayStore") and ensure
imports are alphabetized within the external and internal groups so "useShallow"
sits with other third-party imports.

In `@client/src/components/replay/player/hooks/useReplayPlayer.ts`:
- Around line 15-23: Replace the untyped playerRef and newPlayer uses with a
narrow TypeScript interface describing only the rrweb-player surface your code
uses (e.g., methods like $set, triggerResize, play/pause,
getCurrentTime/setCurrentTime, getDuration or other members referenced in
useReplayPlayer). Change playerRef from useRef<any>(null) to useRef<PlayerHandle
| null>(null) and type the newPlayer variable as PlayerHandle when creating the
player, then update any call sites to respect the nullable ref. Confirm the
exact method names/signatures against the installed rrweb-player version and
keep the interface minimal to cover only the functions used by useReplayPlayer
(playerRef, newPlayer, $set, triggerResize, and any time/control methods).

In `@client/src/components/replay/player/ReplayPlayerTopbar.tsx`:
- Around line 1-12: Imports are out of order: move external package imports
(e.g., Link from "next/link", useParams from "next/navigation", useMemo from
"react", useShallow from "zustand/react/shallow") above internal project imports
(e.g., useGetSessionReplayEvents,
BrowserTooltipIcon/CountryFlagTooltipIcon/DeviceTypeTooltipIcon/OperatingSystemTooltipIcon,
useReplayStore) so that external dependencies appear first, then internal
modules; reorder the import block accordingly to satisfy the project's import
ordering guideline.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ffafd089-90c5-4d4f-b985-f5d468f28c59

📥 Commits

Reviewing files that changed from the base of the PR and between 1304531 and 587b96f.

📒 Files selected for processing (7)
  • client/src/app/[site]/globe/3d/hooks/timelineLayer/useTimelineSessions.ts
  • client/src/app/[site]/globe/components/TimelineScrubber.tsx
  • client/src/app/[site]/globe/page.tsx
  • client/src/app/[site]/replay/components/ReplayCard.tsx
  • client/src/app/[site]/replay/components/ReplayList.tsx
  • client/src/components/replay/player/ReplayPlayerTopbar.tsx
  • client/src/components/replay/player/hooks/useReplayPlayer.ts

Comment on lines +46 to +48
width: widthRef.current,
// subtract for the custom controls
height: height - CONTROLS_HEIGHT,
height: heightRef.current - CONTROLS_HEIGHT,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clamp the derived player height before passing it through.

If height is 0 during initial measurement or a collapsing layout, this computes a negative value and feeds it into both initialization and $set().

🐛 Suggested guard
-            height: heightRef.current - CONTROLS_HEIGHT,
+            height: Math.max(heightRef.current - CONTROLS_HEIGHT, 0),
...
-        height: height - CONTROLS_HEIGHT,
+        height: Math.max(height - CONTROLS_HEIGHT, 0),

Also applies to: 146-149

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/replay/player/hooks/useReplayPlayer.ts` around lines 46
- 48, Derived player height can go negative when heightRef.current is 0; compute
a clamped height (e.g., max(0, heightRef.current - CONTROLS_HEIGHT)) and use
that value instead of heightRef.current - CONTROLS_HEIGHT in the player
initialization and any subsequent $set() calls. Update the code paths that read
widthRef.current and heightRef.current (including the initialization and the
places referenced by the $set() call) to use the clampedHeight variable so you
never pass a negative height into the player.

…seconds directly for improved accuracy in seeking functionality.
@goldflag goldflag merged commit d949e79 into master Mar 12, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant