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

128 lines
11 KiB
Markdown

# 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
* [x] Request audit lists endpoints, call sites, duplicate patterns, and priority.
* [x] Refactor plan defines target data architecture, resource cache policies, invalidation rules, and phased rollout.
* [x] Data client/resource layer is implemented without changing the visible UI design.
* [x] Dashboard → settings/profile/general/divination navigation avoids duplicate profile requests where safe.
* [x] Points balance requests are deduped in-flight and cached/invalidated intentionally.
* [x] Stable package/config-style reads are cached or intentionally left uncached with rationale.
* [x] Authenticated pages keep correct behavior after writes and after 401 responses.
* [ ] Browser verification captures representative flows under mobile and desktop viewports.
* [x] `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.