perf: restore dashboard SSR by scoping the Stream SDK contexts - #1103
Conversation
StreamProvider wrapped {children} in a next/dynamic component with ssr:false.
That skips server rendering for the component AND its children, so no dashboard
markup ever reached the HTML — measured on #1102, where `<h1` never appeared in
the document, "Welcome back" arrived at 4769ms inside the RSC payload, and FCP
sat at ~6000ms no matter what the server did. Suspense can only stream markup
the server is willing to produce.
The connector now renders null as a SIBLING of children and publishes to a
module store read via useSyncExternalStore. Children sit in a fixed position,
so the subtree is server-rendered and the once-per-session remount documented
at StreamProviderImpl (children -> <StreamVideo> -> <Chat> as the sockets
settled) disappears with it — that was the storm behind "I pressed Join ten
times" (#248).
The SDK contexts move to the surfaces that consume them: <Chat> to the three
Messages tabs, <StreamVideo> to /meetings. Safe because every useChatContext
consumer lives under components/chat/, and the sidebar unread badge explicitly
does not use context — useChatUnreadCount reads the StreamChat singleton and
documents that it works outside the provider. ConsulteeAppointmentsAdapter was
the one video consumer left on the dashboard; it now reads the same instance
via getGlobalVideoClient() at click time, which also drops a static SDK import
from a dashboard component.
Unverified until the preview: this is a rendering-shape change with runtime-only
failure modes, so chat and meetings need clicking through, not just CI.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
✅ Deploy Preview for familiarise ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (11)
📝 WalkthroughWalkthroughThe PR moves Stream SDK connection management into a shared external store and browser-only connector. Chat and video providers now wrap only their consuming surfaces. Appointment actions retrieve the global video client when users join sessions. ChangesStream provider split
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant StreamProvider
participant StreamConnector
participant connectionStore
participant StreamChatScope
participant ChatLayout
StreamProvider->>StreamConnector: load browser-only connector
StreamConnector->>connectionStore: publish chat client and connection state
StreamChatScope->>connectionStore: subscribe to state
connectionStore-->>StreamChatScope: provide clients.chat
StreamChatScope->>ChatLayout: render Chat provider around layout
Possibly related PRs
Poem
✨ 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 |
A completeness sweep for useStreamVideoClient/useCall/useChatContext turned up one consumer the first pass missed: useEventActions:115, which powers Join on the consultee and org appointments surfaces. Those routes no longer mount <StreamVideo>, so the hook would have returned undefined and every Join would have hit the "Video client not initialized" toast. Same treatment as ConsulteeAppointmentsAdapter: read the singleton at click time via getGlobalVideoClient(). After this, every remaining video-context consumer lives under app/meetings/ and every chat-context consumer under components/chat/ — which is what makes the scoping safe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
📄 Knowledge reviewDosu skipped reviewing this PR because your organization has used its |




What this fixes
StreamProviderwrapped{children}in anext/dynamiccomponent withssr: false, which skips server rendering for the component and its children. The connector now rendersnullas a sibling ofchildrenand publishes connection state to a module store read viauseSyncExternalStore:Because children never change position, this also removes the once-per-session remount documented in
StreamProviderImpl—children→<StreamVideo>→<Chat>changed the element type at that position as the sockets settled, tearing down the whole dashboard. That was the storm behind "I pressed Join ten times" (#248).The SDK contexts move to the surfaces that consume them:
<Chat>to the three Messages tabs,<StreamVideo>to/meetings.A latent bug fell out of the sweep.
useEventActions:115useduseStreamVideoClient()and powers Join on the consultee and org appointments surfaces — routes that no longer mount<StreamVideo>. Left alone, every Join there would have hit "Video client not initialized". It now reads the singleton at click time viagetGlobalVideoClient(), same asConsulteeAppointmentsAdapter. After this, every remaining video-context consumer lives underapp/meetings/and every chat-context consumer undercomponents/chat/— verified by import-level grep, which is what makes the scoping safe.This does NOT improve FCP — and here is why
Measured on the preview, streaming the document:
Event Planner(layout nav)<h1(page header)animate-pulse(skeleton)Welcome backThere were two SSR blockers stacked. This PR removes the second. The first is upstream and dominates:
app/dashboard/consultant/[consultantId]/layout.tsx:648-654is"use client"and returns<PersonalDashboardShellSkeleton />instead of{children}whenever its client queries are loading — which is always true during SSR, becauseconsultant-datais never server-prefetched (noprefetchQuery/dehydratefor that key anywhere in the repo). So the layout emits a skeleton server-side, its own nav included.Follow-up: add a server layout that prefetches that query and wraps the client layout in
HydrationBoundary.app/dashboard/layout.tsxis already a server component, so the pattern exists in-repo.Merging on the two bug fixes, not on a perf claim.
Verification
CI:
tsc, eslint, 2514 tests — all green.Clicked through on the preview as
consultant_1:useEventActionsfix protects; zero console errors/meetings— layout +StreamVideoScopemount with no context crashNot covered, stated plainly: consultee and org Messages (same code path as consultant, but not clicked), and an actual meeting join, which needs a live call.
Summary by CodeRabbit