Skip to content

perf: restore dashboard SSR by scoping the Stream SDK contexts - #1103

Merged
teetangh merged 2 commits into
devfrom
perf/dashboard-ssr-stream-scope
Aug 3, 2026
Merged

perf: restore dashboard SSR by scoping the Stream SDK contexts#1103
teetangh merged 2 commits into
devfrom
perf/dashboard-ssr-stream-scope

Conversation

@teetangh

@teetangh teetangh commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What this fixes

StreamProvider wrapped {children} in a next/dynamic component with ssr: false, which skips server rendering for the component and its children. The connector now renders null as a sibling of children and publishes connection state to a module store read via useSyncExternalStore:

StreamConnectionContext.Provider          ← stable, SSR-able, SDK-free
├── {children}                            ← fixed position
└── <StreamConnector/>  (ssr:false)       ← renders null, owns the sockets

Because children never change position, this also removes the once-per-session remount documented in StreamProviderImplchildren<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:115 used useStreamVideoClient() 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 via getGlobalVideoClient(), same as ConsulteeAppointmentsAdapter. After this, every remaining video-context consumer lives under app/meetings/ and every chat-context consumer under components/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:

Marker in server HTML Present?
Event Planner (layout nav) false
<h1 (page header) false
animate-pulse (skeleton) true
Welcome back only in the RSC payload, at 4,585 ms

There were two SSR blockers stacked. This PR removes the second. The first is upstream and dominates:

app/dashboard/consultant/[consultantId]/layout.tsx:648-654 is "use client" and returns <PersonalDashboardShellSkeleton /> instead of {children} whenever its client queries are loading — which is always true during SSR, because consultant-data is never server-prefetched (no prefetchQuery/dehydrate for 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.tsx is 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:

  • Consultant Messages — channels list renders, no "Connecting to chat" hang, no error state, sidebar unread badge shows 7, zero console errors/warnings
  • Consultant Appointments — full render (next appointment, counts, list); this is the surface whose Join the useEventActions fix protects; zero console errors
  • /meetings — layout + StreamVideoScope mount with no context crash

Not 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

  • Improvements
    • Improved chat and video connection handling across messaging, meetings, and appointments.
    • Chat and video features now load more reliably without blocking surrounding page content.
    • Video clients are initialized when joining a meeting, improving readiness and reducing connection-related issues.
    • Added more consistent connection state handling and retry support for Stream-powered features.
    • Existing loading, error handling, authentication, navigation, and meeting workflows remain unchanged.

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>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@netlify

netlify Bot commented Aug 3, 2026

Copy link
Copy Markdown

Deploy Preview for familiarise ready!

Name Link
🔨 Latest commit 8c553f3
🔍 Latest deploy log https://app.netlify.com/projects/familiarise/deploys/6a70d3876a9c2a0008a00d8f
😎 Deploy Preview https://deploy-preview-1103--familiarise.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 72 (🟢 up 17 from production)
Accessibility: 99 (🟢 up 3 from production)
Best Practices: 92 (🟢 up 9 from production)
SEO: 99 (no change from production)
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a68df222-f981-4ad1-9cb6-1d66307a1acf

📥 Commits

Reviewing files that changed from the base of the PR and between 1c1d591 and 8c553f3.

📒 Files selected for processing (11)
  • app/dashboard/consultant/[consultantId]/(features)/messages/MessagesTab.tsx
  • app/dashboard/consultee/[consulteeId]/(features)/messages/MessagesTab.tsx
  • app/dashboard/organization/[orgId]/messages/MessagesClient.tsx
  • app/meetings/layout.tsx
  • components/appointments/consultee/ConsulteeAppointmentsAdapter.tsx
  • components/appointments/consultee/useEventActions.ts
  • components/stream/StreamChatScope.tsx
  • components/stream/StreamVideoScope.tsx
  • lib/stream/connection-store.ts
  • providers/StreamProvider.tsx
  • providers/StreamProviderImpl.tsx

📝 Walkthrough

Walkthrough

The 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.

Changes

Stream provider split

Layer / File(s) Summary
Connection store and SDK connector
lib/stream/connection-store.ts, providers/StreamProviderImpl.tsx
The connection store exposes Stream clients, state snapshots, subscriptions, updates, and reset behavior. The connector publishes state, handles retries, and renders no children.
SSR-safe provider shell
providers/StreamProvider.tsx
StreamProvider renders children directly, reads connection state from the store, and mounts the browser-only connector as a sibling.
Scoped chat and video contexts
components/stream/StreamChatScope.tsx, components/stream/StreamVideoScope.tsx
The new scopes dynamically load SDK providers and wrap children only when the corresponding client is available.
Chat and video surface integration
app/dashboard/.../messages/*, app/meetings/layout.tsx, components/appointments/consultee/*
Chat surfaces use StreamChatScope, meetings use StreamVideoScope, and appointment actions retrieve the global video client at join time.

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
Loading

Possibly related PRs

Poem

A rabbit hops where chat streams flow,
Video scopes now softly glow.
Clients wake when joins begin,
Store and providers keep state in sync.
“Squeak!” says the hare, “the routes are clear!”

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/dashboard-ssr-stream-scope

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.

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>
@sonarqubecloud

sonarqubecloud Bot commented Aug 3, 2026

Copy link
Copy Markdown

@teetangh teetangh self-assigned this Aug 3, 2026
@teetangh
teetangh marked this pull request as ready for review August 3, 2026 18:03
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@dosubot

dosubot Bot commented Aug 3, 2026

Copy link
Copy Markdown

📄 Knowledge review

Dosu skipped reviewing this PR because your organization has used its 200 included credits for the month. Your usage will reset on 2026-09-01. To have Dosu review this PR before then, ask your organization admin to upgrade to a pro account.


Leave Feedback Ask Dosu about familiarise_web Add Dosu to your team

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