p1-home-page-production-latency

drafttype/backlogtech/nextjstech/mongodbdomain/performance

p1 — Home page (/) production latency

TL;DR

Production slowness on / comes from a client-only home page that waits for NextAuth session, then fires four separate /api/* GETs before the shell leaves loading. On Vercel + remote MongoDB that often means four serverless cold starts and four DB handshakes. Highest-impact fix: server-fetch initial payload (match dashboard/page.tsx) or a single bootstrap API; align Vercel + Atlas region; measure before micro-optimizing queries.

Problem

Users report high latency when opening / in production. The page feels slow on cold load and sometimes on return visits (API routes are NetworkOnly in the PWA — by design for shared-device safety).

Current load sequence (as-built)

Code touchpoints (app repo)

LayerPathRole
Home pagesrc/app/(main)/page.tsx"use client"; useAppData hook
Session gatesrc/components/navigation/AppShell.tsxSkeleton until useAuth().isLoading
Authsrc/components/auth/AuthContext.tsxuseSession() → blocks data fetch
API clientsrc/lib/utils/api-client.tsFour fetch calls
Routessrc/app/api/categories, bank-accounts, transactions, transactions/category-month-totalsEach via apiHandlerconnectDB + getUserData()
PWAnext.config.mjs/api/* GET = NetworkOnly (no SW cache)
Reference patternsrc/app/(main)/dashboard/page.tsxRSC + Promise.all + Suspense (faster pattern)
Client cachesrc/lib/utils/client-cache.ts, transactions-cache.tslocalStorage cold paint; repeat visits only

Four API calls on first paint

#EndpointPurpose
1/api/categoriesCategory list
2/api/bank-accountsAccount cards
3/api/transactions?limit=10&lean=true&sort=…Recent 10 transactions
4/api/transactions/category-month-totals?startDate&endDate&…Month aggregates for category rows

Static (1+2) and dynamic (3+4) start only after user is set. isLoading stays true until both staticData and transactions exist (monthly totals use monthlyPending separately).

Improvements (priority order)

1. Server-fetch initial payload — highest impact

Refactor / like the dashboard:

  • Server Component page loads bank accounts, categories, recent transactions, and month totals in one request lifecycle (Promise.all, one connectDB, one auth()).
  • Pass props into a thin client HomeClient for modals, filters, outbox, optimistic merges.

Why: Data can stream in the first HTML instead of after JS + session + 4 round-trips. Often 1–3s faster on cold production loads.

2. Bootstrap API — if staying client-first

GET /api/home/bootstrap returning:

{
  "bankAccounts": [],
  "categories": [],
  "transactions": [],
  "monthlyCategoryTotals": []
}

One serverless invocation, one DB connection, one auth check. Keep granular routes for filters and pagination.

Why: Reduces cold-start multiplication from 4 → 1 on Vercel.

3. Infrastructure — often the real culprit

Verify in Vercel + MongoDB Atlas:

  • Same region for Vercel deployment and Atlas cluster (e.g. both Singapore if users are in Indonesia).
  • Connection string tuned for serverless (maxPoolSize / minPoolSize as needed).
  • Atlas Query Insights / slow query log for hot paths.

connectDB caches per function instance (src/lib/mongodb.ts), but each parallel route on a cold instance can still pay connect once.

4. Shorten auth waterfall

Middleware already protects routes (src/middleware.ts). Client still waits on /api/auth/session before fetching.

Options:

  • Pass minimal user from server layout into context so / can fetch immediately.
  • Prefetch bootstrap data in (main) layout for authenticated users.

Session is JWT (auth.config.ts — no DB read per session), but network latency to /api/auth/session remains.

5. Perceived loading polish (smaller diffs)

  • In useLayoutEffect, when both static + tx cache hit, set isLoading: false immediately (today it clears in a later useEffect — possible skeleton flash).
  • Keep category sections on monthlyPending skeleton while bank accounts + tx list paint from cache.
  • On filter change: never setTransactions(null) if cache exists for the new hash (already intended in pwa-performance skill).

6. Database query hygiene

  • Recent list: limit: 10, lean: true — good; index { userId: 1, date: -1, isDeleted: 1 } on transactions.
  • getCategoryMonthTotals: aggregation over all month transactions — can dominate if history is large. Consider pre-aggregated monthly summaries on write, or ensure $match on date is selective early in the pipeline.

7. Bundle / client JS

Modals already use dynamic(..., { ssr: false }). Further wins: lazy-load below-the-fold sections; audit TransactionList and icon imports on the critical path. Lower impact than (1)–(3).

Measurement (do before deep code changes)

SignalWhat to check
BrowserWaterfall: /api/auth/session + four APIs on cold load
VercelFunction duration per route; cold-start count
MongoDBSlow queries on getTransactions and getCategoryMonthTotals

If one API is ~80% of latency → fix that query. If all four are slow only on first hit after idle → cold start × 4.

Acceptance criteria

  • Cold production load to meaningful content (accounts + recent txs) under an agreed budget (e.g. < 2s P75 on target region) — define after baseline measurement.
  • At most one server round-trip for initial home data (RSC or bootstrap), or documented exception.
  • Repeat visit with warm cache: no full-page skeleton when localStorage has valid static + tx cache.
  • No regression to PWA shared-device rule: /api/* GET stays NetworkOnly unless an ADR approves scoped private caching.

Recommended rollout

  1. Measure session + four endpoints on cold production load.
  2. Align Vercel + MongoDB region.
  3. Implement RSC initial fetch (preferred) or bootstrap API.
  4. Polish cache / isLoading behavior for repeat visits.
  5. Optimize aggregation only if Atlas shows category-month-totals as hot.

Implications

If skipped

  • Production / keeps session → four cold API routes on many visits. Users see long skeletons on Vercel + remote MongoDB; you risk abandoning the PWA for daily checks because “it feels slow” even when data and auth are correct.

Why this priority

  • p1 — auth and correctness are shipped; perceived performance on the default route is the next barrier to daily use. Not p0 because the app works; it is p1 because home is where you land after every login and offline sync.

When shipped

  • Meaningful content in one server round-trip (RSC or bootstrap); repeat visits hydrate from cache without full-page skeleton flash.

Dependencies

  • None blocking; auth migration complete ([[Projects/personal-finance-notion/backlog/done/p0-authjs-phase-d-pre-launch-hardening|Phase D done]]).

Related

  • App repo: /Users/mg/Project/personal-finance-notion
  • Skills: .cursor/skills/pwa-performance/SKILL.md, .cursor/skills/caching-strategy/SKILL.md
  • [[Projects/personal-finance-notion/changelog/2026-04-19-offline-first-writes|Offline-first writes changelog]] — localStorage cache design
  • [[Projects/personal-finance-notion/backlog/done/p0-h4-pwa-cache-api-leak|H4 PWA API cache]] — why /api/* is network-only

Source

Cursor analysis session, 2026-05-23 — production latency on /.