From 261233442eb31643002e855f142c031574eaf69c Mon Sep 17 00:00:00 2001 From: Kirill Date: Sun, 24 May 2026 19:23:38 +0500 Subject: [PATCH] docs: add frontend performance optimization design spec --- ...rontend-performance-optimization-design.md | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-24-frontend-performance-optimization-design.md diff --git a/docs/superpowers/specs/2026-05-24-frontend-performance-optimization-design.md b/docs/superpowers/specs/2026-05-24-frontend-performance-optimization-design.md new file mode 100644 index 0000000..3a89d88 --- /dev/null +++ b/docs/superpowers/specs/2026-05-24-frontend-performance-optimization-design.md @@ -0,0 +1,129 @@ +# Frontend Performance Optimization — Design Spec + +**Date:** 2026-05-24 +**Author:** opencode +**Status:** Draft + +## Problem + +Lighthouse performance score: **0.51** + +| Metric | Value | Score | +|--------|-------|-------| +| FCP | 5.5s | 0 | +| LCP | 10.8s | 0 | +| TBT | 10ms | 1 | +| CLS | 0.122 | 0.84 | + +**Root causes:** +- 9.9 MB total transfer size (dev mode, 368 requests) +- maplibre-gl (1.3 MB) loaded on every page, used only in About + AddressMapPicker +- 18 @dicebear packages (~2 MB) statically imported, never code-split +- TipTap (116 KB) always loaded, used only in Messages/Reviews +- Only 2 of 14+ routes lazy-loaded +- Zero React.memo usage — uncontrolled re-renders + +## Architecture + +### 1. Code-split maplibre-gl + +**Files affected:** +- `client/src/main.tsx` — remove global `import 'maplibre-gl/dist/maplibre-gl.css'` +- `client/src/features/address-map-picker/ui/AddressMapPicker.tsx` — dynamic import +- `client/src/pages/about/ui/AboutPage.tsx` — dynamic import + +**Implementation:** +- Remove CSS import from `main.tsx` +- In components that use map, load CSS + JS via `import()` inside `useEffect` or `React.lazy` +- Show loading skeleton while map loads + +### 2. Code-split TipTap + +**Files affected:** +- `client/src/shared/ui/RichTextMessageContent.tsx` — lazy wrapper +- `client/src/shared/ui/RichTextMessageEditor.tsx` — lazy wrapper +- `client/src/features/order-chat/` — add Suspense boundary +- `client/src/widgets/reviews-block/` — add Suspense boundary + +**Implementation:** +- Create lazy exports in `shared/ui/index.ts` +- Wrap TipTap-dependent components in `}>` + +### 3. Code-split dicebear avatar styles + +**Files affected:** +- `client/src/shared/lib/avatar-styles.ts` — dynamic import map + +**Implementation:** +- Replace static imports with factory map: `{ styleName: () => import('@dicebear/style') }` +- Cache loaded styles in `Map` +- Load style on first use, not at module initialization + +### 4. Lazy-load all public routes + +**Files affected:** +- `client/src/app/routes/index.tsx` — convert all routes to lazy + Suspense + +**Routes to lazy-load:** +- `/` (HomePage) +- `/auth` (AuthPage) +- `/auth/callback` (AuthCallbackPage) +- `/cart` (CartPage) +- `/checkout` (CheckoutPage) +- `/about` (AboutPage) +- `/info/*` (InfoPage) +- `/privacy` (PrivacyPolicyPage) +- `/terms` (TermsPage) +- `/products/:id` (ProductPage) + +**Exception:** 404 route stays synchronous (lightweight). + +**Fallback:** `SkeletonPage` for consistency with existing admin/me lazy routes. + +### 5. React.memo on key components + +**Files affected:** +- `client/src/entities/product/ui/ProductCard.tsx` +- `client/src/shared/ui/OptimizedImage.tsx` +- `client/src/shared/ui/UserAvatar.tsx` +- `client/src/app/layout/AppHeader.tsx` + +**Implementation:** +- `ProductCard`: memo with custom comparator comparing `product.id` and `onClick` reference +- `OptimizedImage`: memo comparing `src`, `alt`, `priority` +- `UserAvatar`: memo comparing `userId`, `style` +- `AppHeader`: memo (default shallow comparison sufficient) + +## Data Flow + +``` +User navigates → Router loads lazy route chunk → Component mounts + ↓ + Dynamic imports (maplibre/TipTap) fire + ↓ + Suspense shows fallback → Content renders +``` + +## Error Handling + +- All `React.lazy` components wrapped in `` with fallback +- Dynamic imports wrapped in try/catch with error boundary fallback +- Dicebear style loading: fallback to default style if dynamic import fails + +## Testing + +- Verify routes still work after lazy conversion (manual navigation) +- Verify map loads correctly in AboutPage and AddressMapPicker +- Verify TipTap renders in Messages and Reviews +- Verify avatars render with correct styles +- Run `npm run lint` and `npm test` after changes +- Run `npm run build` to verify production bundle + +## Success Criteria + +- Initial bundle size reduced by ~3-4 MB (dev mode) +- FCP improved from 5.5s to < 2.5s +- LCP improved from 10.8s to < 4.0s +- No regressions in functionality +- All tests pass +- Lint passes