Files
shop-server/docs/superpowers/specs/2026-05-24-frontend-performance-optimization-design.md
T

4.1 KiB

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