Skip to content

perf(dashboard): cut cold-load waterfall and widget fade-in#357

Draft
hugodemenez wants to merge 2 commits into
betafrom
cursor/dashboard-load-perf-dab8
Draft

perf(dashboard): cut cold-load waterfall and widget fade-in#357
hugodemenez wants to merge 2 commits into
betafrom
cursor/dashboard-load-perf-dab8

Conversation

@hugodemenez

Copy link
Copy Markdown
Owner

Why ~3.5s still happens (after #355 prefetch)

Prefetch fixed Connections → Dashboard route warmth. Remaining time is mostly cold / reload work:

  1. Sequential loadData waterfall — auth → layout → trades → userData → calculateAccountMetricsAction (extra trades DB round-trip), with global isLoading / “Loading your trades…” until the end
  2. Sequential trade pagesgetTradesAction fetched 1000-row chunks one after another
  3. 1.5s widget fadeIn — even after data is ready, every widget animates for 1.5s (easy ~1.5s of perceived lag)
  4. (not in this PR) Eager widget-registry JS graph + soft-nav remount of WidgetCanvas

What this PR does

Change Effect
Parallel Promise.all for layout + trades + userData after auth Critical path ≈ max(…) not sum(…)
Client computeMetricsForAccounts instead of server action Drops duplicate trades query
Stripe starts concurrently (does not block isLoading) Spinner clears earlier
Parallel trade cache pages Multi-thousand trade users stop paying sequential page sum
Widget fade 1.5s0.2s Removes ~1.3s of “still loading” feel after paint

Includes the loadData work from #352 (supersedes that PR for the data path).

Out of scope (next)

  • Lazy widget-registry / tab code-splitting (soft-nav remount JS cost)
  • Keep-alive so Dashboard ↔ Connections never unmounts widgets

Test plan

  • bun run typecheck
  • Cold /dashboard: loading toast clears without waiting on Stripe; charts match prior metrics
  • Account with many trades: trades fetch feels faster (parallel pages)
  • Widgets appear without a long fade after data is ready
Open in Web Open in Cursor 

@vercel

vercel Bot commented Jul 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
deltalytix Ready Ready Preview, Comment Jul 21, 2026 7:07am

cursoragent and others added 2 commits July 21, 2026 07:01
Remove the sequential layout → trades → userData → account-metrics
waterfall after auth. Start Stripe concurrently without blocking
isLoading, and compute account metrics client-side from already-loaded
trades instead of a second trades DB round-trip.

Co-authored-by: Hugo Demenez <hugodemenez@users.noreply.github.com>
Fetch getTradesAction cache pages with Promise.all instead of a
sequential loop. Cut widget mount fade from 1.5s to 0.2s so ready
data is not masked by animation.

Co-authored-by: Hugo Demenez <hugodemenez@users.noreply.github.com>
@cursor
cursor Bot force-pushed the cursor/dashboard-load-perf-dab8 branch from 3a99c18 to e15f988 Compare July 21, 2026 07:02

@cursor cursor 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.

Daily review — PR #357

Verdict: Approve (rebased onto beta at e15f988e; merge blocked by token permissions — draft PR also needs manual ready-for-review).

Solid perf PR that attacks the real cold-load bottlenecks: sequential loadData waterfall, duplicate metrics server round-trip, sequential trade pagination, and the 1.5s widget fade masking ready data.

Validation

  • Rebased cleanly onto current beta (resolved conflicts in data-provider.tsx + database.ts; fixed getTradesAction(false) signature drift from beta auth hardening).
  • bun run typecheck passes.

Highlights

  • Promise.all for layout/trades/userData after auth is the right critical-path cut.
  • Client computeMetricsForAccounts matches calculateAccountMetricsAction (same shared util) without a second trades query.
  • Stripe decoupled from isLoading — spinner should clear meaningfully earlier.
  • refreshAllData parallelizes trades/user refresh and recomputes metrics from final store state (good race fix).

Follow-ups (non-blocking)

  • Consider closing superseded PR #352.
  • Optional: cap concurrent trade page fetches (e.g. p-limit) for users with 10k+ trades to avoid DB connection spikes.
  • Out-of-scope items from PR body (lazy widget-registry, route keep-alive) remain good next steps.
Open in Web View Automation 

Sent by Cursor Automation: Coworker

Comment thread context/data-provider.tsx
const userDataPromise = getUserData();

const parallelStarted = performance.now();
const [layoutResult, tradesResult, userDataResult] = await Promise.all([

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.

Nice — this is the core win. Auth stays serial; everything else becomes max(layout, trades, userData) instead of a sum. The timedPromise breakdown in logDashboardPerf will make before/after tuning easy in dev.

Comment thread context/data-provider.tsx
// TODO: Cache layout client side (lightweight)
if (!dashboardLayout) {
const dashboardLayoutResponse = await getDashboardLayout(user.id);
const existingLayout = useUserStore.getState().dashboardLayout;

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.

Skipping getDashboardLayout when the store already has a layout is a good micro-optimization for soft navigations / remounts. Using getState() avoids a stale closure on dashboardLayout.

Comment thread context/data-provider.tsx
data.accounts || []
// Metrics from trades already in hand — avoids a second trades DB fetch via server action.
const metricsStarted = performance.now();
const accountsWithMetrics = computeMetricsForAccounts(

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.

Correct equivalence move: calculateAccountMetricsAction already delegates to this same helper after fetching trades server-side. Reusing loadedTrades here drops a full duplicate DB round-trip.

Comment thread context/data-provider.tsx
withLoading: false,
}),
]);
// refreshUserDataOnly may finish before trades land; recompute from final stores.

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.

Good race handling — refreshUserDataOnly may finish before trades land when run in parallel, so recomputing from final store snapshots here prevents briefly stale account metrics.

Comment thread server/database.ts
const pageTrades = await getCachedTrades(userId, isSubscribed, page, chunkSize)
trades.push(...pageTrades)
}
const pageResults = await Promise.all(

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.

Parallel pagination should cut wall time for large histories. Minor ops note: users with many pages will fan out concurrent Prisma/cache calls — consider a concurrency cap if you see pool pressure in prod.

<div
ref={widgetRef}
className="relative h-full w-full rounded-lg bg-background shadow-[0_2px_4px_rgba(0,0,0,0.05)] group isolate animate-[fadeIn_1.5s_ease-in-out] overflow-clip"
className="relative h-full w-full rounded-lg bg-background shadow-[0_2px_4px_rgba(0,0,0,0.05)] group isolate animate-[fadeIn_0.2s_ease-out] overflow-clip"

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.

1.5s → 0.2s is a big perceived-latency win once data is ready. ease-out reads snappier than the old ease-in-out for mount.

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