docs: add frontend performance optimization design spec

This commit is contained in:
Kirill
2026-05-24 19:23:38 +05:00
parent bc417375b5
commit 261233442e
@@ -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 `<Suspense fallback={<Skeleton>}>`
### 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<string, Style>`
- 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 `<Suspense>` 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