Files
eryao/.trellis/tasks/05-10-audit-and-optimize-web-performance/prd.md
T
2026-05-10 20:29:42 +08:00

11 KiB

Audit and optimize web performance

Goal

Comprehensively audit and refactor the web frontend data interaction layer while preserving the existing UI. The refactor should reduce repeated HTTP requests, hide US-backend latency where safe, centralize cache/invalidation behavior, and make authenticated page transitions feel faster in both perceived and actual request-count terms.

What I already know

  • Backend is hosted in the US, so users far away experience noticeable request latency.
  • User has observed repeated HTTP requests and avoidable data-loading waits.
  • Web stack is Astro 6 + React 19 + React Router DOM under web/.
  • Auth state lives in web/src/lib/auth.ts; business APIs live in web/src/lib/api.ts.
  • Existing getPointsBalance() has a 1-minute in-memory TTL cache, but many other stable reads do not.
  • Early code scan found repeated getUserProfile() calls across AppShell, LoginForm, SettingsPage, GeneralSettingsPage, and ProfileDetailPage.
  • AppShell already loads profile globally and exposes UserSettingsContext; some pages still refetch profile instead of using that context.
  • Auth refresh now has single-flight behavior from the previous fix, which helps avoid concurrent refresh waste.

Assumptions

  • Performance improvements should preserve backend source of truth and auth safety.
  • Cached authenticated data must be invalidated after writes that change it.
  • We should prefer a local, typed data-client/resource layer over adding a large state/query dependency unless audit shows the custom approach is too risky.
  • Optimizations should be observable through request count reduction and improved perceived loading behavior.

Requirements

  • Audit all authenticated web pages for duplicate or avoidable requests.
  • Define and implement a frontend data layer with caching, in-flight dedupe, stale-while-revalidate, prefetch, and explicit invalidation.
  • Reuse AppShell-loaded profile/settings where possible instead of refetching.
  • Add cache invalidation for profile/settings/points changes.
  • Keep UI presentation unchanged at rest; only data source and loading/refresh behavior should change.
  • Avoid stale or unsafe auth behavior: 401 and refresh failures must still clear auth and redirect.
  • Improve perceived performance with cached data, route-level reuse, and fewer full-page loading waits.
  • Document verification with before/after request counts for key flows.

Acceptance Criteria

  • Request audit lists endpoints, call sites, duplicate patterns, and priority.
  • Refactor plan defines target data architecture, resource cache policies, invalidation rules, and phased rollout.
  • Data client/resource layer is implemented without changing the visible UI design.
  • Dashboard → settings/profile/general/divination navigation avoids duplicate profile requests where safe.
  • Points balance requests are deduped in-flight and cached/invalidated intentionally.
  • Stable package/config-style reads are cached or intentionally left uncached with rationale.
  • Authenticated pages keep correct behavior after writes and after 401 responses.
  • Browser verification captures representative flows under mobile and desktop viewports.
  • git diff --check passes; build/typecheck status documented.

Definition of Done

  • PRD updated with final implementation notes.
  • Targeted code changes committed.
  • Performance audit and refactor plan recorded in the task directory.
  • Any reusable conventions captured in .trellis/spec/web/index.md or relevant spec.

Out of Scope

  • Backend endpoint redesign.
  • CDN/edge deployment changes.
  • Offline mode.
  • Large dependency adoption unless explicitly approved after audit.
  • Visual redesign of existing pages.

Technical Notes

  • Follow .trellis/spec/web/index.md.
  • All API paths remain in web/src/lib/api-routes.ts.
  • Components should call typed API helpers from web/src/lib/api.ts, not inline fetch('/api/...').
  • Target refactor plan: .trellis/tasks/05-10-audit-and-optimize-web-performance/refactor-plan.md.
  • Request audit: .trellis/tasks/05-10-audit-and-optimize-web-performance/request-audit.md.
  • Current build is blocked by existing Astro adapter configuration (NoAdapterInstalled) and should be documented unless fixed in a separate task.

Implementation Notes - 2026-05-10

  • Added web/src/lib/data-client.ts, a typed in-memory query cache with TTL, in-flight request dedupe, peek, set, prefix invalidate, prefetch, subscribe, and clearAll.
  • Added web/src/lib/resources.ts for profile, points, packages, history list/thread/summary, notifications list, and unread-count cache policy plus React resource hooks.
  • Moved points TTL behavior out of api.ts; getPointsBalance() is now transport-only and points caching lives in the resource layer.
  • AppShell now loads profile through the profile resource, subscribes to profile cache changes, clears data cache via clearAuth(), prefetches cheap dashboard data after auth, and prefetches route resources on nav hover/focus.
  • Settings, general settings, profile detail, dashboard, store, history list, notifications, manual/auto divination, divination processing, result, and follow-up pages now reuse resource caches instead of owning duplicate GET lifecycles.
  • Mutations patch or invalidate shared cache: profile/settings/avatar set profile; notification read/all-read patch list and unread count; divination/follow-up completion invalidates points and history.
  • Quality check fixed two post-implementation issues: invalidated active resources now refetch instead of staying empty/stale, and useHistoryThread(undefined) no longer emits a doomed request when result pages rely on router/session state.
  • Verification: npm exec astro sync passed; git diff --check passed; npm run build remains blocked by existing Astro NoAdapterInstalled; temporary tsc --noEmit check reports existing DashboardApp.tsx translation-map typing errors after refactor-specific type issues were fixed.
  • Browser verification note: the in-app browser was switched to a visible 390x844 mobile viewport with the dev server on 127.0.0.1:4322, but the automation surface only exposed the in-app browser toolbar during this run, so request-count capture still needs a follow-up pass with a usable page automation surface.

Implementation Notes - 2026-05-10 Second Pass

  • Fixed the Astro build blocker without breaking existing direct-refresh history URLs by adding the @astrojs/node adapter and switching the web build to server output. The six prerender = false dynamic history shell pages remain in place for /{locale}/history/:id and /{locale}/history/:id/followup.
  • New in-app links still use the static shell URLs with threadId query parameters: /{locale}/history/result?threadId=... and /{locale}/history/followup?threadId=..., while the legacy direct URLs continue to resolve through Astro and React Router.
  • Split authenticated page bodies in DashboardApp.tsx with React.lazy and Suspense, keeping AppShell, nav config, auth guard, and link interception eager. The initial DashboardApp chunk dropped from the prior ~142 KB shape to ~54 KB in the built output, with heavy pages emitted as separate chunks.
  • Optimized static image assets in place under web/design/assets/images while preserving filenames and references. Largest qigua result images are now about 125-130 KB each, coin images about 26-27 KB, logo about 42 KB, and tutorial images reduced from roughly 800 KB / 2.5 MB / 3.0 MB to about 372 KB / 1.0 MB / 1.2 MB.
  • Added primeHistoryThreadFromSnapshot() so dashboard/history-list clicks seed the thread resource from an already-loaded history snapshot before navigating to the result page. This avoids an immediate duplicate thread GET when list data already contains the target thread.
  • Added real web typecheck dependencies (@astrojs/check, typescript, @types/node) and fixed the surfaced type gaps: DashboardApp now uses typed i18n sections, settings route shells reuse DashboardAppPage.astro, and the about-page ICP fields are present in all locales.

Verification - 2026-05-10 Second Pass

  • npm run build in web/: passed. Astro built server output with @astrojs/node, preserving on-demand dynamic history shells.
  • npm exec astro sync in web/: passed.
  • git diff --check: passed.
  • npm exec tsc -- --noEmit: passed.
  • npm exec astro check: passed with 0 errors; remaining diagnostics are hints for pre-existing cleanup candidates.
  • Server preview smoke via astro preview on 127.0.0.1:4322: GET /zh/dashboard, /zh/history/smoke-thread, /zh/history/smoke-thread/followup, /zh/history/result?threadId=smoke-thread, /zh/history/followup?threadId=smoke-thread, /en/history/smoke-thread, and /zh_Hant/history/smoke-thread/followup all returned 200 text/html.
  • Built client chunks: DashboardApp is ~54 KB, with lazy page chunks emitted separately (ManualDivinationPage ~19 KB, AutoDivinationPage ~19 KB, DivinationResultPage ~14 KB, HistoryFollowUpPage ~9 KB, HistoryListPage ~7 KB). Built asset folders: dist/client/_astro ~520 KB and dist/client/images ~4.2 MB.

Implementation Notes - 2026-05-10 Third Pass

  • Removed the authenticated layout-level AuthProvider wrapper and deleted the unused provider/context file. AppShell remains the only authenticated session recovery and route-guard owner for dashboard routes.
  • Login and existing-session checks now use the shared profile resource instead of calling getUserProfile() directly, so login-to-dashboard can reuse the profile cache that was already fetched for language redirect decisions.
  • Successful email login clears the shared data cache before storing the new session to avoid carrying authenticated resource data across account changes.

Verification - 2026-05-10 Third Pass

  • npm exec astro sync in web/: passed.
  • npm exec tsc -- --noEmit in web/: passed.
  • npm exec astro check in web/: passed with 0 errors; remaining diagnostics are hints.
  • npm run build in web/: passed. Vite reported the existing auth.ts mixed static/dynamic import chunking warning.
  • git diff --check: passed.

Implementation Notes - 2026-05-10 Fourth Pass

  • Fresh email-code login submit now redirects using the locale implied by the submitted backend language (localeToBackendLanguage(locale) -> backendLanguageToLocale(language)) instead of fetching profile before navigation. This removes the avoidable profile GET whose in-memory cache was discarded by the full-page dashboard load.
  • Existing-auth login-page recovery still fetches profile after token refresh because the stored profile language preference is the authoritative source for redirecting an already-authenticated user from the login page. The current resource layer is intentionally in-memory only, so it cannot provide a reload-safe handoff for window.location.href; optimizing the fresh-login side avoids the waste while preserving stored-preference redirects for recovered sessions.

Verification - 2026-05-10 Fourth Pass

  • npm exec tsc -- --noEmit in web/: passed.
  • npm exec astro check in web/: passed with 0 errors; remaining diagnostics are hints.
  • npm run build in web/: passed. Vite reported the existing auth.ts mixed static/dynamic import chunking warning.
  • git diff --check: passed.