Issue: #269
Scope: Client-side rendering performance across core marketplace pages
| Page / Scope | Budget (ms) | Rationale |
|---|---|---|
marketplace_load |
1 500 | First-contentful paint target for browse grid |
prompt_detail_load |
1 000 | Product page — user is one click from purchase |
browse_load |
1 200 | Discovery page with filter controls |
sell_form_load |
800 | Creator flow — form should feel instant |
profile_load |
1 000 | Profile / settings tab |
wallet_connect |
3 000 | Network round-trip expected; Freighter extension latency |
purchase_flow |
5 000 | Stellar transaction + confirmation |
Budgets are enforced client-side via src/lib/observability/performanceAudit.ts. Any measurement that exceeds its budget emits a perf_budget_exceeded_total metric.
Severity: Medium
Affected scope: marketplace_load
The <Marketplace> page renders all items into the DOM simultaneously. With 50+ prompts this causes a measurable layout recalculation spike (~200 ms on a mid-range device). The grid uses CSS grid with no window virtualisation.
Recommendation: Introduce @tanstack/react-virtual for the prompt card grid once the item count regularly exceeds 20.
Severity: Medium
Affected scope: prompt_detail_load
src/pages/prompts/[id].tsx fetches preview data on every mount with no cache layer. Navigating away and back triggers a full 350 ms (mocked) round-trip.
Recommendation: Wrap the fetch in a useQuery call with staleTime: 60_000 so revisits are served from cache immediately.
Severity: Low
Affected scope: sell_form_load
CreatePromptForm.tsx imports encryptPromptPlaintext and wrapPromptKey from @/lib/crypto/promptCrypto. These pull in the SubtleCrypto polyfill path, adding ~18 kB gzip to the sell-page chunk.
Recommendation: Move the crypto imports behind a dynamic import() inside handleSubmit so the chunk is only fetched when the user actually attempts to submit.
Severity: Low
Affected scope: marketplace_load, browse_load
Prompt cover images and creator avatars are rendered with plain <img> tags without loading="lazy" or explicit width/height attributes, causing cumulative layout shift (CLS) during load.
Recommendation: Add loading="lazy" and explicit dimensions, or migrate to a Next.js <Image> equivalent (if the SPA is ever wrapped in Next.js) or a Vite image plugin.
Severity: Low
Affected scope: profile_load
The profile page mounts all <TabsContent> blocks eagerly. Switching to the "Settings" tab while the "Listings" data is still loading causes both subtrees to re-render.
Recommendation: Pass forceMount={false} (Radix UI default) and confirm that the Tabs primitive unmounts inactive content — or wrap heavy content in React.memo.
| File | What was added |
|---|---|
src/lib/observability/performanceAudit.ts |
startAudit(scope) → returns stop(metadata?), accumulates entries, emits metrics |
src/hooks/usePerformanceAudit.ts |
React hook: auto-starts on mount, markDone() records duration |
src/debug/components/PerformanceAuditPanel.tsx |
Dev-only panel showing per-scope summary and recent entries |
src/pages/Marketplace.tsx |
usePerformanceAudit({ scope: "marketplace_load" }) + markDone when query settles |
- Instrument remaining pages (
Browse,PromptPreview,Sell,Profile) withusePerformanceAudit. - Connect
metrics.emitto a real sink (Prometheus push-gateway, Datadog RUM, or a lightweight/api/metricsendpoint) once the backend is production-ready. - Set up a Lighthouse CI step in GitHub Actions to catch regressions on each PR.
- Revisit virtualisation threshold after first real-traffic data from the metrics sink.