perf(react-sdk): reduce re-renders and event-listener churn#2273
perf(react-sdk): reduce re-renders and event-listener churn#2273oliverlaz wants to merge 1 commit into
Conversation
Apply React SDK performance-audit fixes: - memoize hot-path components (ParticipantView, Audio, CallParticipantListingItem) to stop re-render cascades when the participants array re-emits on every speaking/mute tick - mark useScrollPosition scroll listeners passive (handlers never preventDefault) - depend on destructured primitives in useDragToScroll so pointer listeners are not re-subscribed on every render - drive the device-settings audio level via a ref instead of ~12.5Hz state updates - cache diacritic-normalized participant names per component instance and match with includes() instead of building a RegExp from user input - stabilize the SearchInput escape handler with useEffectEvent - hoist the static absolute-position style object in Video Also clear the Safari/Firefox srcObject setTimeout in BaseVideo on cleanup to close a use-after-cleanup play() race.
|
📝 WalkthroughWalkthroughThis PR introduces performance optimizations across the React SDK by memoizing high-frequency render components, refactoring search algorithms to avoid regex overhead with caching, switching audio visualization from state to ref-based DOM updates, improving timer and event cleanup, and refining hook dependencies. ChangesReact SDK Performance Optimizations
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/react-sdk/src/core/components/Video/BaseVideo.tsx (1)
20-41:⚠️ Potential issue | 🟡 MinorUse
useLayoutEffectinBaseVideofor media stream (srcObject) binding.
packages/react-sdk/src/core/components/Video/BaseVideo.tsxcurrently usesuseEffectto setvideoElement.srcObject/playback; changing it touseLayoutEffectavoids paint with stale media state (and aligns withVideo.tsx, which already usesuseLayoutEffect).Proposed change
-import { ComponentPropsWithRef, forwardRef, useEffect, useState } from 'react'; +import { ComponentPropsWithRef, forwardRef, useLayoutEffect, useState } from 'react'; ... - useEffect(() => { + useLayoutEffect(() => { if (!videoElement || !stream) return; if (stream === videoElement.srcObject) return; ... - }, [stream, videoElement]); + }, [stream, videoElement]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/react-sdk/src/core/components/Video/BaseVideo.tsx` around lines 20 - 41, The BaseVideo component uses useEffect to assign videoElement.srcObject, play(), and cleanup which can cause a paint with stale media; replace the existing useEffect hook in BaseVideo.tsx with useLayoutEffect so the srcObject assignment, Safari/Firefox timeout+play logic, and cleanup (clearTimeout, pause, srcObject = null) run synchronously before paint; keep the existing timeout logic, error handling for play(), and the dependency array ([stream, videoElement]) unchanged so behavior matches Video.tsx while preventing visual glitches.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/styling/src/DeviceSettings/DeviceSettings-layout.scss`:
- Around line 132-133: Add a blank line before the inline double-slash comment
preceding the transform rule to satisfy
scss/double-slash-comment-empty-line-before: insert an empty line above the line
"// resting state; the live level is written inline by AudioVolumeIndicator" in
DeviceSettings-layout.scss so the comment is separated from the previous rule,
leaving the "transform: scaleX(0);" declaration unchanged.
---
Outside diff comments:
In `@packages/react-sdk/src/core/components/Video/BaseVideo.tsx`:
- Around line 20-41: The BaseVideo component uses useEffect to assign
videoElement.srcObject, play(), and cleanup which can cause a paint with stale
media; replace the existing useEffect hook in BaseVideo.tsx with useLayoutEffect
so the srcObject assignment, Safari/Firefox timeout+play logic, and cleanup
(clearTimeout, pause, srcObject = null) run synchronously before paint; keep the
existing timeout logic, error handling for play(), and the dependency array
([stream, videoElement]) unchanged so behavior matches Video.tsx while
preventing visual glitches.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 846c2c5e-bbcc-4d7d-bf6e-ddb52e057156
📒 Files selected for processing (11)
packages/react-sdk/src/components/CallParticipantsList/CallParticipantListingItem.tsxpackages/react-sdk/src/components/CallParticipantsList/CallParticipantsList.tsxpackages/react-sdk/src/components/DeviceSettings/AudioVolumeIndicator.tsxpackages/react-sdk/src/components/Search/SearchInput.tsxpackages/react-sdk/src/core/components/Audio/Audio.tsxpackages/react-sdk/src/core/components/ParticipantView/ParticipantView.tsxpackages/react-sdk/src/core/components/Video/BaseVideo.tsxpackages/react-sdk/src/core/components/Video/Video.tsxpackages/react-sdk/src/hooks/useDragToScroll.tspackages/react-sdk/src/hooks/useScrollPosition.tspackages/styling/src/DeviceSettings/DeviceSettings-layout.scss
💡 Overview
Performance and hygiene fixes for
@stream-io/video-react-sdk, from a structured performance audit. Three focus areas: re-render reduction on hot paths, event-listener hygiene, and one use-after-cleanup correctness fix. No public API changes.📝 Implementation notes
React.memoon hot-path components (ParticipantView,Audio,CallParticipantListingItem): the participants array emits a fresh reference on every speaking/mute/connection-quality tick, so these rows/tiles reconciled on every emission even when their own props were unchanged. Memoizing stops the cascade (ParticipantViewis wrapped asmemo(forwardRef(...))).scrolllisteners (useScrollPosition): both handlers only read scroll geometry and never callpreventDefault, so{ passive: true }lets the browser composite scroll frames without waiting on JS. Listener removal is unaffected.useDragToScrolldependencies: the sole caller passes an inline options object, so the effect re-subscribed all four pointer listeners on every render. Now depends on destructured primitives (enabled,decay,minVelocity,dragThreshold). Thepointermovelistener stays non-passive (it callspreventDefault).AudioVolumeIndicator: replaced a ~12.5 HzsetState(driving one CSS transform) with a direct ref write; the SCSS now sets ascaleX(0)resting state.CallParticipantsList): the per-keystrokenormalizeString(p.name)over all participants plusnew RegExp(userInput)is replaced with a component-scoped, lazily-created normalized-name cache and anincludes()substring match (also removes a latent ReDoS / invalid-pattern smell). The cache is released when the list unmounts, so user-controlled names are not retained across calls.SearchInput: the escape-key handler is stabilized withuseEffectEvent, so thekeydownlistener is no longer re-subscribed whenexitSearchidentity changes.Video: hoisted the static{ position: 'absolute' }style object so it does not defeat memoized custom placeholders.BaseVideo(correctness fix): the Safari/FirefoxsetTimeout(..., 0)that re-assignssrcObjectand callsplay()was never cleared. Astream/element change or unmount within the same tick fired it on an element the cleanup had just nulled, producingplay() interrupted/AbortErrornoise. The timeout is now cleared in cleanup.Verified: ESLint and
tsc --noEmitclean on the touched files.🎫 Ticket: https://linear.app/stream/issue/XYZ-123
📑 Docs: N/A (no public API change)
Summary by CodeRabbit
New Features
Bug Fixes
Performance