Repository navigation
perf: eliminate server-side request waterfalls - #3391
Conversation
Parallelize independent server-component fetches across ui, code, and playground apps per Vercel React best-practices (rules 1.3-1.5). No behavioral changes; only request start timing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFivAdY2xhHim6s1mWPR8G
WalkthroughThe changes start independent data requests earlier across code, playground, and dashboard pages. Authentication checks, redirects, project fallbacks, and eligibility handling remain in place. Some playground project-fetch errors now reject the combined request. ChangesConcurrent loading updates
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 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
🤖 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 `@apps/ui/src/app/dashboard/`[orgId]/layout.tsx:
- Around line 29-37: Defer creation of initialProjectsDataPromise in the
dashboard layout until after the isAuthorizedForOrg check succeeds. Likewise, in
apps/ui/src/app/dashboard/[orgId]/[projectId]/page.tsx at line 32, create
initialActivityDataPromise only after the developer-role redirect; both sites
must prevent protected requests before their respective UI authorization checks.
🪄 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: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 68a92ac4-1892-49f5-ba0b-6f6f4fcf5027
📒 Files selected for processing (11)
apps/code/src/app/dashboard/feedback/dev-plan-cancellation/page.tsxapps/code/src/app/dashboard/survey/page.tsxapps/playground/src/app/audio/page.tsxapps/playground/src/app/canvas/page.tsxapps/playground/src/app/image/page.tsxapps/playground/src/app/playground-shell.tsxapps/playground/src/app/realtime/page.tsxapps/playground/src/app/video/page.tsxapps/ui/src/app/dashboard/[orgId]/[projectId]/page.tsxapps/ui/src/app/dashboard/[orgId]/layout.tsxapps/ui/src/app/dashboard/page.tsx
Weekly React/Next.js best-practices audit of the four Next.js apps (`apps/ui`, `apps/code`, `apps/playground`, `apps/docs`) against [Vercel's react-best-practices guide](https://github.com/vercel-labs/agent-skills/blob/main/skills/react-best-practices/AGENTS.md). The three previous audits (#3391, #3518, #3647) are all merged, so this week covers code merged since then plus remaining findings, including two items those audits explicitly deferred that turned out to have surgical fixes. No routing changes, no user-visible behavior changes, nothing touching `export const dynamic`. ## Fixes ### apps/ui - **Prevent Hydration Mismatch Without Flickering (rule 6.5)** — `lib/components/sidebar.tsx`, `app/dashboard/[orgId]/layout.tsx`. The sidebar open state was read from localStorage in a post-mount effect (behind a `mounted` flag), so every dashboard load rendered the default state and then snapped to the persisted one — a layout shift on every navigation for anyone with a collapsed sidebar. Deferred by the last two audits as needing prop threading; it doesn't: the provider's only mount point is the org server layout, which already reads cookies. The state now persists in a `sidebar_state` cookie (the pattern the playground's sidebar already uses) and the layout passes it as `defaultOpen`, so the first paint is correct and both effects plus the `mounted` state are gone. One-time migration cost: a previously saved localStorage value is ignored, so a collapsed sidebar renders expanded once until the user toggles again. - **Per-Request Deduplication with React.cache() (rule 3.9)** — `app/dashboard/page.tsx`. The dashboard entry page fetched `/user/me`, `/orgs`, and `/orgs/{id}/projects` through the raw fetcher while `dashboard/layout.tsx` fetches `/user/me` through the deduped `getUserMe()` in the same render pass — a duplicate round-trip on every `/dashboard` hit. All three now go through the existing `cache()`-backed helpers, which also lets the redirect target's org layout share them. - **Dynamic Imports for Heavy Components (rule 2.4)** — `lib/utils/markdown.tsx`. The prism-based `SyntaxHighlightedPre` (prism-react-renderer plus its full `themes` barrel) was statically wired into the markdown options used by the blog, guides, changelog, legal, use-cases, and migration routes, shipping the highlighter on articles with zero code blocks. Now `next/dynamic`, mirroring the identical fix `apps/code` got in #3518. - **Cache Repeated Function Calls / Hoist Constructors (rules 7.4/7.10)** — `components/api-keys/api-keys-list.tsx`, `api-key-limit-fields.tsx`, `api-key-ttl-fields.tsx`, `components/master-keys/master-keys-list.tsx`, `lib/components/number-ticker.tsx`. The API-keys table constructed 3–4 `Intl.DateTimeFormat` instances per row per render (creation date, tooltip, expiry, period reset), the master-keys list the same, and `NumberTicker` constructed one per spring animation frame (×3 tickers on pages using it). All formatters are hoisted to module scope (or, for the ticker, created once per subscription). ### apps/code - **Minimize Serialization at RSC Boundaries / bundle size (rules 3.6, 2.x)** — new `lib/coding-models.ts`, `components/CodingModelsShowcase.tsx`, `app/page.tsx`, `app/coding-models/page.tsx`. The showcase — rendered on the landing page and `/coding-models` — imported the entire `@llmgateway/models` catalogue (~800K of source: every model with all provider mappings) into a client component, then derived a few fields per coding model in the browser. Flagged as the app's biggest bundle finding in #3518 and deferred twice as "needs an interface redesign"; the redesign is small. All derivation (DevPass coding gate, recommended/premium sets, cheapest-provider pricing) now runs server-side and only a trimmed card array (8 scalar fields per model) crosses the RSC boundary. The client keeps just the tab state and copy button; rendered UI is unchanged. - **NumberTicker per-frame `Intl.NumberFormat`** — `components/ui/number-ticker.tsx`, same fix as the ui copy above. ### apps/playground - **Promise.all() for Independent Operations (rule 1.5)** — `app/realtime/page.tsx`. Realtime was the only media page still awaiting `/orgs/{id}/projects` serially after models/providers/orgs; the siblings (`image`, `video`, `audio`, `canvas`) all start it eagerly in the same `Promise.all` when the URL carries an `orgId`. Realtime now does the same, with the eager result used only when that org actually ends up selected. - **Defer Non-Critical Third-Party Libraries (rule 2.3)** — `lib/stripe.ts`, `components/credits/top-up-credits-dialog.tsx`. `useStripe()` loaded Stripe.js (~200KB, phones home on load) in an unconditional mount effect, and the top-up dialog mounts closed on the chat, image, video, and audio pages — so every playground visit fetched Stripe. Ported the `enabled` gate `apps/code` got in #3647; the dialog passes its `open` state, so Stripe.js loads only when the dialog is actually opened. - **Bounded module cache (rule 4.4-adjacent)** — `components/ai-elements/code-block.tsx`. The shiki `tokensCache` retained the full `ThemedToken[][]` of every code block ever rendered (including one entry per streaming snapshot) for the tab's lifetime; its sibling caches were already bounded/cleaned. Now LRU-capped at 200 entries. ### apps/docs - **Defer Await Until Needed (rule 1.2)** — `app/api/chat/route.ts`. The Ask-AI FlexSearch index was built eagerly at module scope: importing the route read and indexed the processed text of all ~131 MDX pages even when `DOCS_AI_SUPPORT_CHAT_API_KEY` is unset and the handler always 503s — and the module-scope promise had no rejection handler until the first tool call, so an indexing failure at boot would crash the standalone server as an unhandled rejection. The index is now built lazily and memoized on first search, with failed builds dropped so a transient error doesn't stick. - **Unnecessary `"use client"` on static components** — new `components/tracked-link.tsx`; `components/enterprise-cta.tsx`, `ai-tooling-cards.tsx`, `self-host-cards.tsx`. Three purely presentational card components were client components solely to fire a PostHog click capture — `EnterpriseCTA` renders in the TOC footer of every docs page, so its markup shipped as client JS everywhere. A thin `TrackedLink` client wrapper now owns the capture, and the cards (icons, copy, layout) are server components. - **Bounded module cache** — `components/ai/page-actions.tsx`. The "Copy Markdown" cache stored each copied page's entire raw markdown in an unbounded module `Map`; now capped with oldest-entry eviction, matching the treatment `markdown.tsx` got in #3518. - **Cache Storage API Calls (rule 7.5)** — `components/ai/search.tsx`. The Ask-AI input wrote its draft to localStorage synchronously on every keystroke; now debounced (300ms), flushed/cleared on submit. - **Remove always-missing `useMemo`** — `components/ai/search.tsx`. The context value was memoized on `[chat, open, setOpen]`, but `useChat` returns a fresh object every render, so the memo allocated every time and never hit; removed (React Compiler covers the rest). ## Considered and deliberately skipped - **Anything involving `export const dynamic`** — intentional (runtime env loading); excluded per repo policy. - **docs: `APIPage` in `mdx-components.tsx`** pulls the fumadocs OpenAPI playground into the client manifest of every docs page — the largest remaining docs bundle item, but the app has a single catch-all page route serving both prose and API reference, so splitting it needs a route restructure. Deferred. - **docs: static `posthog-js` import in the root provider** — the init is already idle-deferred; deferring the import itself requires reworking how `PostHogProvider` receives its client instance. Deferred as behavior-sensitive. - **ui: `posthog.identify()` inside `getUser()`** runs on every dashboard layout render (server-side twin of the client issue fixed in #3518/#3647) — relocating identification to the auth path is an analytics-behavior decision, not a perf-only diff. - **ui: security-events page hand-rolls `useEffect` fetching** (same class as the already-deferred audit-logs/routing-config pages) — the right fix is a `useInfiniteQuery` rewrite; too invasive for this pass. - **ui: millisecond-precision `new Date()` in the agents-view query key** defeats its `staleTime` — real, but truncating the window boundary changes the queried range semantics slightly; left for a deliberate change. - **playground: chat route's project retrieval serialized ahead of MCP connects** — parallelizing changes error-ordering on the hottest route; same reasoning as #3518's deferral of that route's auth/body ordering. - **playground: `@streamdown/mermaid` statically registered for every assistant message** — potentially the largest chat-chunk item, but needs bundle analysis to confirm the plugin doesn't lazy-load internally before acting. - **Manual memoization nits** — all four apps run the React Compiler; only issues the compiler cannot fix (effects, module caches, per-frame constructors, RSC boundaries) were touched. ## Verification - `pnpm build`: 15 of 17 workspaces green, including docs, code, and playground. `ui#build` compiles and type-checks, but static export fails on `/compare/litellm/opengraph-image` with `SELF_SIGNED_CERT_IN_CHAIN` — the same pre-existing build-environment artifact documented and reproduced on clean main in #3647 (TLS-intercepting proxy breaking a build-time `next/og` fetch), unrelated to this diff. - `pnpm exec tsc --noEmit` in `apps/ui` passes. - `pnpm format` clean. - No visual changes intended: the sidebar renders in its persisted state without the previous post-hydration snap, and the showcase/dialog/card changes render identical UI — so no before/after screenshots. Note: this session pushes to its designated branch, so the head branch is `claude/upbeat-johnson-mc6akz` rather than the `chore/react-bp-audit-2026-08-24` naming convention (same situation as #3518); future audits should locate this PR by title. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01GQLYmwthWTnfTLViZsfYKM --- _Generated by [Claude Code](https://claude.ai/code/session_01GQLYmwthWTnfTLViZsfYKM)_ <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Updated coding-model showcases with refreshed model details, pricing, context limits, and recommendation badges. - Sidebar preferences now persist across sessions. - Realtime views load projects for the selected workspace more reliably. - Added consistent click tracking for documentation and promotional links. - **Bug Fixes** - Improved documentation search reliability and retry behavior. - Preserved AI search drafts more reliably while typing. - Deferred Stripe loading until the credit top-up dialog opens. - **Performance** - Improved code highlighting, markdown rendering, number formatting, and dashboard data loading. - Added bounded caching to keep documentation and code previews responsive. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude <noreply@anthropic.com>
Weekly React/Next.js best-practices audit of the four Next.js apps (
apps/ui,apps/code,apps/playground,apps/docs), using Vercel's React best-practices guide as the rulebook. All fixes this week fall in the guide's highest-impact category, Eliminating Waterfalls (CRITICAL) — sequentialawaits of independent requests in server components. None of the changes alter rendering, routing, or user-visible behavior; they only change when independent requests start. All offetchServerData,fetchModels, andfetchProviderscatch internally and never reject, so eagerly started promises cannot produce unhandled rejections on early-return/redirect paths.Fixes
Rule 1.5 — Promise.all() for Independent Operations
apps/ui/src/app/dashboard/[orgId]/layout.tsx—/user/me,/orgs, and/orgs/{id}/projectswere awaited one after another (3 sequential round trips to the API on every dashboard navigation). The three requests are independent, so they now start together; the projects result is consumed only after the org-authorization check, same as before.apps/playground/src/app/realtime/page.tsx— the/orgsfetch ran only after models + providers resolved; it now joins their existingPromise.all.Rule 1.4 — Prevent Waterfall Chains (start independent operations immediately)
apps/ui/src/app/dashboard/page.tsx—/orgswaited for/user/meto finish even though it doesn't depend on it. Both now start immediately; the login redirect still happens before the orgs result is consumed.apps/ui/src/app/dashboard/[orgId]/[projectId]/page.tsx— the/activityfetch (the page's main payload) waited for the/orgsrole check. Both now start together; the developer-role redirect still occurs before activity data is used.apps/code/src/app/dashboard/survey/page.tsxandapps/code/src/app/dashboard/feedback/dev-plan-cancellation/page.tsx— the eligibility fetch waited for/user/me. Both start together now; unauthenticated users still get redirected before the eligibility result is consumed.apps/playground/src/app/playground-shell.tsx—fetchModels()/fetchProviders()(the model catalogue for the main chat page) started only after the entire org/billing chain (/orgs→/chat-plans/status→/playground/chat-org→ projects) completed. The promises now start first and are awaited where they always were. The org/billing chain itself is left strictly sequential — its ordering is intentional (comments in the file: the funded-org redirect must run before the chat-org fetch so redirected users never get a Chat org provisioned).Rule 1.3 — Dependency-Based Parallelization
apps/playground/src/app/{video,image,audio,canvas}/page.tsx(4 files, identical pattern) — the request chain was 4 sequential blocks: models+providers →/playground/chat-org→/orgs→/orgs/{id}/projects. Only one edge is a real dependency (/orgs?includeChat=truemust run after the chat-org ensure call, per the in-code comment). That edge is kept as a.then()chain and everything else now runs in a singlePromise.all, collapsing 4 awaits into the longest single chain. The removedtry/catcharound the projects fetch was dead code —fetchServerDatanever throws (it logs and returnsnullinternally).Considered and deliberately skipped
export const dynamicdeclarations — untouched everywhere; they are intentional and required for loading environment variables at runtime.apps/uidashboard layout'sawait import("content-collections")(rule 2.5, statically analyzable imports) — the dynamic import is guarded by a try/catch because content collections may be unavailable during build; converting it to a top-level import could break builds, so left alone.generateMetadata+ page both fetch the share) — already deduplicated by Next.js request memoization; same for theapps/codepublic profile page (same URL + options within one render pass).lucide-react/date-fns/rechartsbarrel imports (rule 2.1) — already covered: these packages are in Next.js's defaultoptimizePackageImportslist (andapps/playgroundconfigures it explicitly).useEffect(rules 4.4/6.5) — existing usages (sidebar state, dismissed banners) are already hydration-safe post-mount reads; no change warranted.apps/docs— pages are statically generated withgenerateStaticParams; thegetGithubLastEditcall runs at build/revalidate time, not per-request. Nothing actionable found.Verification
pnpm build— 17/17 tasks successfulpnpm format— clean (no diffs beyond the fixes)🤖 Generated with Claude Code
https://claude.ai/code/session_01BFivAdY2xhHim6s1mWPR8G
Generated by Claude Code
Summary by CodeRabbit