perf: finish web performance pass
This commit is contained in:
@@ -80,3 +80,48 @@ Comprehensively audit and refactor the web frontend data interaction layer while
|
||||
* 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.
|
||||
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
# Research: Remaining web performance bottlenecks after 1e4871e
|
||||
|
||||
- **Query**: Deeply audit the web frontend for remaining performance bottlenecks after commit 1e4871e. Focus on repeated authenticated requests still possible, first-load route waterfall, oversized static assets, bundle/code-splitting opportunities, build-layer blocker (`NoAdapterInstalled`), dev/prod config, and measurable request/cache improvements.
|
||||
- **Scope**: mixed
|
||||
- **Date**: 2026-05-10
|
||||
|
||||
## Findings
|
||||
|
||||
### Files Found
|
||||
|
||||
| File Path | Description |
|
||||
|---|---|
|
||||
| `web/src/lib/data-client.ts:77` | In-memory query cache with in-flight dedupe, TTL, stale-while-revalidate, invalidation, subscriptions. |
|
||||
| `web/src/lib/resources.ts:46` | Resource keys, TTLs, hooks, prefetch policies, and mutation invalidation wrappers. |
|
||||
| `web/src/components/AppShell.tsx:74` | Authenticated shell boot sequence: refresh token, load profile resource, then prefetch dashboard basics. |
|
||||
| `web/src/components/AuthProvider.tsx:37` | Separate auth recovery wrapper used by `AppLayout`, causing another refresh lifecycle before/alongside `AppShell`. |
|
||||
| `web/src/components/DashboardApp.tsx:1` | Eagerly imports every authenticated route component into one React island. |
|
||||
| `web/src/components/DashboardAppPage.astro:26` | Mounts the full dashboard app with `client:only="react"` for every authenticated route page. |
|
||||
| `web/src/pages/*/history/[id].astro:2` | Dynamic history result routes opt out of prerendering with `export const prerender = false`. |
|
||||
| `web/src/pages/*/history/[id]/followup.astro:2` | Dynamic follow-up routes opt out of prerendering with `export const prerender = false`. |
|
||||
| `web/astro.config.mjs:6` | No Astro adapter configured; Vite dev proxy points `/api` at production API. |
|
||||
| `web/src/layouts/App.astro:19` | Loads full Material Symbols variable font CSS from Google on authenticated app pages. |
|
||||
| `web/src/layouts/Marketing.astro:20` | Loads same full Material Symbols font CSS on marketing pages. |
|
||||
| `web/design/assets/images/**` | Source static images include multiple 0.6 MB to 3.0 MB PNG/JPG files. |
|
||||
| `web/public/` | Only contains favicon files; current code references `/images/logo.png` and `/images/qigua/*.jpg`. |
|
||||
| `web/dist/**` | Existing build output from an earlier run contains copied large image assets and one large dashboard JS chunk; current build cannot regenerate it. |
|
||||
|
||||
### Code Patterns
|
||||
|
||||
#### Priority 1: build remains blocked, which prevents reliable production measurement
|
||||
|
||||
`pnpm run build` fails:
|
||||
|
||||
```text
|
||||
[NoAdapterInstalled] Cannot use server-rendered pages without an adapter.
|
||||
```
|
||||
|
||||
The blocker is directly explained by six dynamic Astro route files that export `prerender = false`:
|
||||
|
||||
```astro
|
||||
// web/src/pages/en/history/[id].astro:2
|
||||
export const prerender = false;
|
||||
```
|
||||
|
||||
The same pattern exists in:
|
||||
|
||||
- `web/src/pages/en/history/[id].astro:2`
|
||||
- `web/src/pages/zh/history/[id].astro:2`
|
||||
- `web/src/pages/zh_Hant/history/[id].astro:2`
|
||||
- `web/src/pages/en/history/[id]/followup.astro:2`
|
||||
- `web/src/pages/zh/history/[id]/followup.astro:2`
|
||||
- `web/src/pages/zh_Hant/history/[id]/followup.astro:2`
|
||||
|
||||
These routes do not use server-only data. They only read `Astro.params.id` and render the same client-side `DashboardAppPage`:
|
||||
|
||||
```astro
|
||||
// web/src/pages/en/history/[id].astro:4-10
|
||||
import DashboardAppPage from '../../../components/DashboardAppPage.astro';
|
||||
|
||||
const locale = 'en' as const;
|
||||
const { id } = Astro.params;
|
||||
---
|
||||
|
||||
<DashboardAppPage locale={locale} />
|
||||
```
|
||||
|
||||
Implementation steps:
|
||||
|
||||
1. If deployment target is static hosting, remove `export const prerender = false` and add a static fallback strategy for client-owned history routes, e.g. Astro static dynamic route support with `getStaticPaths()` only if finite paths exist, or replace file-based `[id]` pages with a static catch-all/client app entry supported by the host rewrite rules.
|
||||
2. If deployment target requires on-demand rendering, add the correct Astro server adapter in `web/astro.config.mjs` and the matching package in `web/package.json`.
|
||||
3. Make build success the gate before bundle-size, image, and request-count acceptance data are considered final.
|
||||
|
||||
#### Priority 2: first authenticated load can still spend requests on duplicate auth/session work
|
||||
|
||||
`AppLayout` wraps every authenticated route with `AuthProvider client:load`:
|
||||
|
||||
```astro
|
||||
// web/src/layouts/App.astro:23-25
|
||||
<AuthProvider client:load>
|
||||
<slot />
|
||||
</AuthProvider>
|
||||
```
|
||||
|
||||
`AuthProvider` refreshes on mount regardless of whether the access token is still fresh:
|
||||
|
||||
```tsx
|
||||
// web/src/components/AuthProvider.tsx:37-45
|
||||
const auth = getAuth();
|
||||
if (!auth?.refresh_token) {
|
||||
clearAuth();
|
||||
redirectToLogin();
|
||||
return;
|
||||
}
|
||||
refreshAccessToken()
|
||||
```
|
||||
|
||||
`AppShell` then has its own auth boot sequence and also calls `refreshAccessToken()` before loading profile:
|
||||
|
||||
```tsx
|
||||
// web/src/components/AppShell.tsx:82-86
|
||||
refreshAccessToken()
|
||||
.then((data) => {
|
||||
if (alive) setAuthUser(data.user);
|
||||
return getProfileResource();
|
||||
})
|
||||
```
|
||||
|
||||
The single-flight refresh promise in `web/src/lib/auth.ts:172` dedupes concurrent refreshes, but this is still a redundant lifecycle. Depending on hydration timing, it can be one shared refresh or two sequential refresh calls. It also delays the authenticated shell behind two components that both show full-screen loading spinners.
|
||||
|
||||
Implementation steps:
|
||||
|
||||
1. Collapse authenticated-route auth boot into one owner. Prefer `AppShell` because it already owns profile, locale redirect, nav prefetch, and user context.
|
||||
2. Remove `AuthProvider` from `AppLayout` if no routed child consumes `useAuth()`, or turn it into a passive context seeded from `getAuth()` without forcing refresh.
|
||||
3. In the remaining boot path, call `refreshAccessToken()` only when `isTokenExpired()` is true; otherwise seed `authUser` from `getAuth().user`.
|
||||
4. Measure initial `/zh/dashboard` load request count before and after. Target: avoid a refresh request when the token is fresh, and keep one refresh maximum when expired.
|
||||
|
||||
#### Priority 3: login to dashboard still repeats profile/session requests
|
||||
|
||||
`LoginForm` checks existing sessions and submit success by calling `getUserProfile()` directly:
|
||||
|
||||
```tsx
|
||||
// web/src/components/LoginForm.tsx:59-63
|
||||
await refreshAccessToken();
|
||||
const profile = await getUserProfile();
|
||||
const userLocale = backendLanguageToLocale(profile.settings?.preferences?.language || 'zh-CN');
|
||||
window.location.href = `/${userLocale}/dashboard`;
|
||||
```
|
||||
|
||||
```tsx
|
||||
// web/src/components/LoginForm.tsx:100-104
|
||||
await loginWithEmail(email, code, language, timezone);
|
||||
const profile = await getUserProfile();
|
||||
const userLocale = backendLanguageToLocale(profile.settings?.preferences?.language || language);
|
||||
window.location.href = `/${userLocale}/dashboard`;
|
||||
```
|
||||
|
||||
Because the redirect reloads the app, the in-memory profile resource is empty on the dashboard. `AppShell` then fetches profile again through `getProfileResource()` at `web/src/components/AppShell.tsx:85`.
|
||||
|
||||
Implementation steps:
|
||||
|
||||
1. Short term: after login, redirect to `/${backendLanguageToLocale(language)}/dashboard` without the extra profile read when the user selected/submitted the language and backend stores it.
|
||||
2. If backend user preference must remain authoritative, persist only the minimal post-login locale/profile seed into `sessionStorage` and hydrate `profileKey` before `AppShell` calls `getProfileResource()`.
|
||||
3. Long term: have the auth/session response include profile language/settings, which eliminates the post-login profile GET entirely.
|
||||
4. Target measurable improvement: login success path should drop from `login + profile + dashboard profile` to `login + dashboard profile`, or to `login` if profile seed is safe.
|
||||
|
||||
#### Priority 4: invalidation can actively create duplicate refetches
|
||||
|
||||
The resource layer intentionally uses one key for history summary and list:
|
||||
|
||||
```ts
|
||||
// web/src/lib/resources.ts:49-50
|
||||
export const historyListKey = ['history', 'list'] as const;
|
||||
export const historySummaryKey = historyListKey;
|
||||
```
|
||||
|
||||
But invalidation calls both keys:
|
||||
|
||||
```ts
|
||||
// web/src/lib/resources.ts:290-293
|
||||
export function invalidateHistory(threadId?: string): void {
|
||||
invalidate(historySummaryKey);
|
||||
invalidate(historyListKey);
|
||||
if (threadId) invalidate(historyThreadKey(threadId));
|
||||
}
|
||||
```
|
||||
|
||||
Since the two keys are the same object/value, the first `invalidate()` deletes the entry and notifies subscribers, which can immediately start a fetch and store an in-flight promise. The second `invalidate()` can delete that new in-flight entry and notify again, allowing a second network request for the same history list.
|
||||
|
||||
Points have a similar repeated invalidation pattern during run flows:
|
||||
|
||||
```tsx
|
||||
// web/src/components/DivinationProcessingOverlay.tsx:157-159
|
||||
const { threadId, runId } = await enqueueDivinationRun(params, yaoStates);
|
||||
invalidatePoints();
|
||||
```
|
||||
|
||||
```tsx
|
||||
// web/src/components/DivinationProcessingOverlay.tsx:257-259
|
||||
setStep('done');
|
||||
invalidatePoints();
|
||||
invalidateHistory(threadId);
|
||||
```
|
||||
|
||||
Follow-up does the same around SSE completion:
|
||||
|
||||
```tsx
|
||||
// web/src/components/HistoryFollowUpPage.tsx:166-190
|
||||
const { runId } = await enqueueFollowUpRun(threadId, text, resultData);
|
||||
invalidatePoints();
|
||||
...
|
||||
invalidateHistory(threadId);
|
||||
invalidatePoints();
|
||||
const snapshot = await getHistoryThreadResource(threadId, true);
|
||||
```
|
||||
|
||||
Implementation steps:
|
||||
|
||||
1. Make `invalidateHistory()` dedupe equal prefixes before invalidating, or stop aliasing `historySummaryKey` and `historyListKey`.
|
||||
2. Add an option to mark entries stale without deleting active in-flight promises, e.g. `invalidate(prefix, { refetchActive: true, preserveInFlight: true })`.
|
||||
3. In divination/follow-up flows, prefer one points invalidation at the moment the backend has committed the charge, or use a stale mark after enqueue and one forced reload at finish.
|
||||
4. Add instrumentation in `data-client.ts` around `startFetch()` in dev builds to count key-level fetch starts. Target: one `['points','balance']` refresh and one history refresh per completed run.
|
||||
|
||||
#### Priority 5: first-load route waterfall and bundle shape are still dominated by one eager app island
|
||||
|
||||
Every authenticated route page renders the same `DashboardAppPage`, and that mounts one client-only React app:
|
||||
|
||||
```astro
|
||||
// web/src/components/DashboardAppPage.astro:26-27
|
||||
<AppLayout locale={locale}>
|
||||
<DashboardApp client:only="react" locale={locale} translations={translations} />
|
||||
</AppLayout>
|
||||
```
|
||||
|
||||
`DashboardApp.tsx` eagerly imports all route screens:
|
||||
|
||||
```tsx
|
||||
// web/src/components/DashboardApp.tsx:3-15
|
||||
import AppShell from './AppShell';
|
||||
import Dashboard from './Dashboard';
|
||||
import StorePage from './StorePage';
|
||||
import HistoryListPage from './HistoryListPage';
|
||||
import DivinationResultPage from './DivinationResultPage';
|
||||
import HistoryFollowUpPage from './HistoryFollowUpPage';
|
||||
import NotificationPage from './NotificationPage';
|
||||
import ProfileDetailPage from './ProfileDetailPage';
|
||||
import SettingsPage from './SettingsPage';
|
||||
import GeneralSettingsPage from './GeneralSettingsPage';
|
||||
import FeedbackPage from './FeedbackPage';
|
||||
import ManualDivinationPage from './ManualDivinationPage';
|
||||
import AutoDivinationPage from './AutoDivinationPage';
|
||||
```
|
||||
|
||||
This means `/dashboard` pays parse/compile/download cost for store, notifications, profile edit, manual casting, auto casting, result rendering, follow-up chat, feedback, and settings code before any route transition.
|
||||
|
||||
Existing stale `web/dist` output from 2026-05-10 01:07 showed:
|
||||
|
||||
| Asset | Raw | Gzip |
|
||||
|---|---:|---:|
|
||||
| `web/dist/_astro/client.Bncfyed9.js` | 185,936 bytes | 58,384 bytes |
|
||||
| `web/dist/_astro/DashboardApp.Df7kJHO-.js` | 142,394 bytes | 39,455 bytes |
|
||||
| `web/dist/_astro/animations.BXuKIGyB.css` | ~89 KiB | 16,947 bytes |
|
||||
|
||||
These numbers are not current-build authoritative because `pnpm run build` now fails, but they confirm the dashboard app chunk is large enough to justify code splitting once the build blocker is fixed.
|
||||
|
||||
Implementation steps:
|
||||
|
||||
1. Convert route components in `DashboardApp.tsx` to `React.lazy(() => import('./RoutePage'))` and wrap `<Routes>` in a small Suspense fallback that preserves shell layout.
|
||||
2. Keep `AppShell`, `Dashboard`, and core nav in the initial chunk; lazy-load lower-probability paths such as store, result, follow-up, profile edit, feedback, notifications, manual/auto casting.
|
||||
3. Split heavy local copy objects in `ManualDivinationPage.tsx`, `AutoDivinationPage.tsx`, and `DivinationProcessingOverlay.tsx` with the route component rather than the dashboard shell.
|
||||
4. Add bundle budget reporting after the build blocker is fixed.
|
||||
|
||||
#### Priority 6: static image assets are missing from `public/` but oversized in source/stale dist
|
||||
|
||||
Current source references these public URLs:
|
||||
|
||||
```tsx
|
||||
// web/src/components/AppShell.tsx:156
|
||||
<img src="/images/logo.png" alt="MeiYao" className="w-9 h-9 rounded-lg" />
|
||||
```
|
||||
|
||||
```tsx
|
||||
// web/src/components/ManualDivinationPage.tsx:61-63
|
||||
<img
|
||||
src={face === 'zi' ? '/images/qigua/zi.jpg' : '/images/qigua/hua.jpg'}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// web/src/components/DivinationResultPage.tsx:24-27
|
||||
if (normalized.includes('上上')) return '/images/qigua/shangshang.jpg';
|
||||
if (normalized.includes('中上')) return '/images/qigua/zhongshang.jpg';
|
||||
if (normalized.includes('下下')) return '/images/qigua/xiaxia.jpg';
|
||||
return '/images/qigua/zhongxia.jpg';
|
||||
```
|
||||
|
||||
But `web/public/` currently contains only:
|
||||
|
||||
- `web/public/favicon.ico` (655 B)
|
||||
- `web/public/favicon.svg` (749 B)
|
||||
|
||||
The same assets exist under `web/design/assets/images/**` and stale `web/dist/images/**`. Largest source assets:
|
||||
|
||||
| File | Size |
|
||||
|---|---:|
|
||||
| `web/design/assets/images/tutorial/tutorial_3.png` | 3.0 MB |
|
||||
| `web/design/assets/images/tutorial/tutorial_2.png` | 2.5 MB |
|
||||
| `web/design/assets/images/qigua/xiaxia.jpg` | 1.4 MB |
|
||||
| `web/design/assets/images/tutorial/tutorial_1.png` | 799 KB |
|
||||
| `web/design/assets/images/qigua/shangshang.jpg` | 768 KB |
|
||||
| `web/design/assets/images/qigua/zhongxia.jpg` | 601 KB |
|
||||
| `web/design/assets/images/qigua/zhongshang.jpg` | 596 KB |
|
||||
| `web/design/assets/images/logo.png` | 142 KB |
|
||||
|
||||
Implementation steps:
|
||||
|
||||
1. Decide whether `web/public/images/**` should be source-controlled. If yes, copy optimized web assets there rather than relying on stale `dist/`.
|
||||
2. Resize/compress qigua images to their displayed dimensions and convert to WebP/AVIF with JPG fallback only if needed.
|
||||
3. Replace `logo.png` with optimized SVG or small WebP/PNG; 142 KB is too high for a small nav/login logo.
|
||||
4. Do not ship tutorial PNGs unless a web tutorial page references them. If they are needed later, lazy-load and optimize them.
|
||||
5. Add an asset budget check after build works, e.g. fail if any single public image exceeds 200 KB without explicit rationale.
|
||||
|
||||
#### Priority 7: Material Symbols font load is broad and render-blocking
|
||||
|
||||
Both app and marketing layouts include the full Material Symbols variable CSS:
|
||||
|
||||
```astro
|
||||
// web/src/layouts/App.astro:19
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
||||
```
|
||||
|
||||
```astro
|
||||
// web/src/layouts/Marketing.astro:20
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
||||
```
|
||||
|
||||
The app also has a local `Icon.tsx` component for many icons, so the font is mostly used by settings/store/feedback subpages. The font CSS is loaded before route-specific need and from a third-party origin.
|
||||
|
||||
Implementation steps:
|
||||
|
||||
1. Replace remaining `material-symbols-rounded` spans with `Icon.tsx` where icons already exist.
|
||||
2. If the font remains necessary, self-host a subset or load it only in authenticated route chunks that use it.
|
||||
3. Add `preconnect` only if the remote font remains.
|
||||
|
||||
#### Priority 8: dev/prod API config is not explicit enough for repeatable performance verification
|
||||
|
||||
`apiBase()` depends on `PUBLIC_API_URL`:
|
||||
|
||||
```ts
|
||||
// web/src/lib/api-client.ts:1-4
|
||||
const apiBase = (): string => import.meta.env.PUBLIC_API_URL || '';
|
||||
export function apiUrl(path: string): string {
|
||||
return path.startsWith('http') ? path : `${apiBase()}${path}`;
|
||||
}
|
||||
```
|
||||
|
||||
Dev server proxy is hardcoded to production:
|
||||
|
||||
```mjs
|
||||
// web/astro.config.mjs:17-24
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'https://api.meeyao.com',
|
||||
changeOrigin: true,
|
||||
secure: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
This makes local request-count testing hit the production-latency path by default, and production behavior depends on `PUBLIC_API_URL` being set correctly in the hosting environment.
|
||||
|
||||
Implementation steps:
|
||||
|
||||
1. Add `.env.example` documenting `PUBLIC_API_URL` for production and local verification.
|
||||
2. Make dev proxy target configurable, e.g. `DEV_API_PROXY_TARGET ?? 'https://api.meeyao.com'`.
|
||||
3. During perf verification, record whether requests go same-origin proxy or direct API origin, because browser connection reuse and CORS/preflight behavior differ.
|
||||
|
||||
### External References
|
||||
|
||||
- [Astro routing reference](https://docs.astro.build/ja/reference/routing-reference/) — Astro routes prerender by default in static mode; exporting `prerender = false` opts a page into on-demand server rendering.
|
||||
- [Astro NoAdapterInstalled error reference](https://docs.astro.build/zh-tw/reference/errors/no-adapter-installed/) — server-rendered pages require an appropriate deployment adapter.
|
||||
|
||||
### Related Specs
|
||||
|
||||
- `.trellis/spec/web/index.md` — Web-specific guidance referenced by the task PRD.
|
||||
- `.trellis/tasks/05-10-audit-and-optimize-web-performance/prd.md` — Active task goals, acceptance criteria, prior implementation notes.
|
||||
- `.trellis/tasks/05-10-audit-and-optimize-web-performance/request-audit.md` — Original request topology and duplicate request plan.
|
||||
- `.trellis/tasks/05-10-audit-and-optimize-web-performance/refactor-plan.md` — Target data-client/resource architecture that commit `1e4871e` implemented.
|
||||
|
||||
## Caveats / Not Found
|
||||
|
||||
- `pnpm run build` currently fails with `NoAdapterInstalled`, so current production bundle sizes could not be regenerated.
|
||||
- `web/dist/**` exists but is stale relative to the current failing build. Sizes from `web/dist` are useful directional evidence only.
|
||||
- `pnpm exec astro sync` passed.
|
||||
- `pnpm exec tsc --noEmit` could not run because `typescript`/`tsc` is not installed in `web/node_modules`.
|
||||
- Browser request-count capture was not run in this research pass. The task PRD already notes a previous automation blocker; request-count verification should happen after the build/route blocker and auth boot duplication are addressed.
|
||||
Reference in New Issue
Block a user