Skip to content

perf: eliminate server-side request waterfalls - #3391

Merged
smakosh merged 1 commit into
mainfrom
chore/react-bp-audit-2026-08-03
Aug 3, 2026
Merged

smakosh merged 1 commit into
mainfrom
chore/react-bp-audit-2026-08-03

Conversation

@smakosh

@smakosh smakosh commented Aug 3, 2026 •

Copy link
Copy Markdown
Member

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) — sequential awaits 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 of fetchServerData, fetchModels, and fetchProviders catch 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}/projects were 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 /orgs fetch ran only after models + providers resolved; it now joins their existing Promise.all.

Rule 1.4 — Prevent Waterfall Chains (start independent operations immediately)

  • apps/ui/src/app/dashboard/page.tsx — /orgs waited for /user/me to 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 /activity fetch (the page's main payload) waited for the /orgs role check. Both now start together; the developer-role redirect still occurs before activity data is used.
  • apps/code/src/app/dashboard/survey/page.tsx and apps/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=true must 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 single Promise.all, collapsing 4 awaits into the longest single chain. The removed try/catch around the projects fetch was dead code — fetchServerData never throws (it logs and returns null internally).

Considered and deliberately skipped

  • export const dynamic declarations — untouched everywhere; they are intentional and required for loading environment variables at runtime.
  • apps/ui dashboard layout's await 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.
  • Playground share page double-fetch (generateMetadata + page both fetch the share) — already deduplicated by Next.js request memoization; same for the apps/code public profile page (same URL + options within one render pass).
  • lucide-react/date-fns/recharts barrel imports (rule 2.1) — already covered: these packages are in Next.js's default optimizePackageImports list (and apps/playground configures it explicitly).
  • Playground shell org/billing chain — intentionally sequential (see above); only the independent catalogue fetches were hoisted.
  • localStorage reads in 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 with generateStaticParams; the getGithubLastEdit call runs at build/revalidate time, not per-request. Nothing actionable found.

Verification

  • pnpm build — 17/17 tasks successful
  • pnpm format — clean (no diffs beyond the fixes)
  • Pre-commit lint-staged (eslint + prettier) passed on all changed files

🤖 Generated with Claude Code

https://claude.ai/code/session_01BFivAdY2xhHim6s1mWPR8G


Generated by Claude Code

Summary by CodeRabbit

  • Performance Improvements
    • Improved loading times across dashboard, survey, feedback, audio, canvas, image, video, realtime, and playground pages by loading related data concurrently.
    • Reduced wait times when retrieving models, providers, organizations, projects, activities, and eligibility information.
  • Bug Fixes
    • Project-loading failures are now surfaced consistently instead of being silently logged, improving error visibility and handling.

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

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

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

Changes

Concurrent loading updates

Layer / File(s) Summary
Eligibility request concurrency
apps/code/src/app/dashboard/feedback/dev-plan-cancellation/page.tsx, apps/code/src/app/dashboard/survey/page.tsx
Feedback and survey pages start eligibility requests before completing authentication checks.
Playground data request orchestration
apps/playground/src/app/audio/page.tsx, apps/playground/src/app/canvas/page.tsx, apps/playground/src/app/image/page.tsx, apps/playground/src/app/playground-shell.tsx, apps/playground/src/app/realtime/page.tsx, apps/playground/src/app/video/page.tsx
Playground pages overlap model, provider, organization, and conditional project requests. Project-fetch failures now propagate from the combined request on applicable pages.
Dashboard request orchestration
apps/ui/src/app/dashboard/page.tsx, apps/ui/src/app/dashboard/[orgId]/layout.tsx, apps/ui/src/app/dashboard/[orgId]/[projectId]/page.tsx
Dashboard pages start organization, user, project, and activity requests earlier and reuse their promises during authentication, redirects, and rendering.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: steebchen

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: removing server-side request waterfalls through concurrent data fetching.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/react-bp-audit-2026-08-03

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 44da286 and 16531f4.

📒 Files selected for processing (11)
  • apps/code/src/app/dashboard/feedback/dev-plan-cancellation/page.tsx
  • apps/code/src/app/dashboard/survey/page.tsx
  • apps/playground/src/app/audio/page.tsx
  • apps/playground/src/app/canvas/page.tsx
  • apps/playground/src/app/image/page.tsx
  • apps/playground/src/app/playground-shell.tsx
  • apps/playground/src/app/realtime/page.tsx
  • apps/playground/src/app/video/page.tsx
  • apps/ui/src/app/dashboard/[orgId]/[projectId]/page.tsx
  • apps/ui/src/app/dashboard/[orgId]/layout.tsx
  • apps/ui/src/app/dashboard/page.tsx

Comment thread apps/ui/src/app/dashboard/[orgId]/layout.tsx
@smakosh
smakosh added this pull request to the merge queue Aug 3, 2026
Merged via the queue into main with commit fdccf9c Aug 3, 2026
12 checks passed
@smakosh
smakosh deleted the chore/react-bp-audit-2026-08-03 branch August 3, 2026 10:29
smakosh added a commit that referenced this pull request Aug 24, 2026
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>
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.

2 participants