dramaling-vocab-learning/docs/04_technical/performance.md

12 KiB

性能優化指南

🚀 性能目標

指標 目標 工具
First Contentful Paint (FCP) < 1.8s Lighthouse
Largest Contentful Paint (LCP) < 2.5s Web Vitals
First Input Delay (FID) < 100ms Web Vitals
Cumulative Layout Shift (CLS) < 0.1 Web Vitals
Time to Interactive (TTI) < 3.8s Lighthouse
Bundle Size < 200KB (gzipped) Webpack Bundle Analyzer

📦 打包優化

1. 代碼分割策略

// next.config.js
module.exports = {
  experimental: {
    optimizeCss: true,
  },
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.optimization.splitChunks = {
        chunks: 'all',
        cacheGroups: {
          default: false,
          vendors: false,
          framework: {
            name: 'framework',
            chunks: 'all',
            test: /(?<!node_modules.*)[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
            priority: 40,
            enforce: true,
          },
          lib: {
            test(module) {
              return module.size() > 160000 &&
                /node_modules[/\\]/.test(module.identifier())
            },
            name(module) {
              const hash = crypto.createHash('sha1')
              hash.update(module.identifier())
              return hash.digest('hex').substring(0, 8)
            },
            priority: 30,
            minChunks: 1,
            reuseExistingChunk: true,
          },
          commons: {
            name: 'commons',
            chunks: 'all',
            minChunks: 2,
            priority: 20,
          },
          shared: {
            name(module, chunks) {
              return crypto
                .createHash('sha1')
                .update(chunks.reduce((acc, chunk) => acc + chunk.name, ''))
                .digest('hex')
            },
            priority: 10,
            minChunks: 2,
            reuseExistingChunk: true,
          },
        },
      }
    }
    return config
  },
}

2. 動態導入

// 使用動態導入減少初始載入
import dynamic from 'next/dynamic'

// 延遲載入大型組件
const FlashcardEditor = dynamic(
  () => import('@/components/FlashcardEditor'),
  {
    loading: () => <Skeleton />,
    ssr: false, // 客戶端渲染
  }
)

// 條件載入
const AdminPanel = dynamic(
  () => import('@/components/AdminPanel'),
  {
    loading: () => <div>Loading admin panel...</div>,
  }
)

// 路由級別代碼分割
export default function Page() {
  const [showEditor, setShowEditor] = useState(false)

  return (
    <div>
      {showEditor && <FlashcardEditor />}
    </div>
  )
}

3. Tree Shaking

// utils/index.ts
// ❌ 錯誤:導出所有
export * from './helpers'

// ✅ 正確:具名導出
export { formatDate, parseJSON } from './helpers'

// 使用時
// ❌ 錯誤:導入整個庫
import * as utils from '@/utils'

// ✅ 正確:只導入需要的
import { formatDate } from '@/utils'

🖼️ 圖片優化

1. Next.js Image 優化

// components/OptimizedImage.tsx
import Image from 'next/image'

export function OptimizedImage({ src, alt }: { src: string; alt: string }) {
  return (
    <Image
      src={src}
      alt={alt}
      width={800}
      height={600}
      placeholder="blur" // 模糊預覽
      blurDataURL="data:image/jpeg;base64,..." // Base64 預覽圖
      loading="lazy" // 延遲載入
      quality={85} // 圖片品質
      sizes="(max-width: 768px) 100vw,
             (max-width: 1200px) 50vw,
             33vw"
    />
  )
}

2. 響應式圖片

// utils/imageOptimization.ts
export function generateImageSizes(src: string) {
  const sizes = [640, 750, 828, 1080, 1200, 1920, 2048, 3840]

  return {
    src,
    srcSet: sizes
      .map(size => `${src}?w=${size} ${size}w`)
      .join(', '),
    sizes: '(max-width: 640px) 100vw, (max-width: 1200px) 50vw, 33vw',
  }
}

渲染優化

1. React 組件優化

// components/FlashcardList.tsx
import { memo, useMemo, useCallback } from 'react'

// 使用 memo 避免不必要的重新渲染
const FlashcardItem = memo(({ card, onSelect }: FlashcardItemProps) => {
  return (
    <div onClick={() => onSelect(card.id)}>
      {card.word}
    </div>
  )
}, (prevProps, nextProps) => {
  // 自定義比較函數
  return prevProps.card.id === nextProps.card.id &&
         prevProps.card.word === nextProps.card.word
})

export function FlashcardList({ cards }: { cards: Card[] }) {
  // 使用 useCallback 避免函數重新創建
  const handleSelect = useCallback((id: string) => {
    console.log('Selected:', id)
  }, [])

  // 使用 useMemo 快取計算結果
  const sortedCards = useMemo(() => {
    return [...cards].sort((a, b) => a.word.localeCompare(b.word))
  }, [cards])

  return (
    <div>
      {sortedCards.map(card => (
        <FlashcardItem
          key={card.id}
          card={card}
          onSelect={handleSelect}
        />
      ))}
    </div>
  )
}

2. 虛擬滾動

// components/VirtualList.tsx
import { FixedSizeList } from 'react-window'

export function VirtualFlashcardList({ cards }: { cards: Card[] }) {
  const Row = ({ index, style }: { index: number; style: any }) => (
    <div style={style}>
      <FlashcardItem card={cards[index]} />
    </div>
  )

  return (
    <FixedSizeList
      height={600} // 容器高度
      itemCount={cards.length}
      itemSize={80} // 每項高度
      width="100%"
    >
      {Row}
    </FixedSizeList>
  )
}

3. Suspense 與並行渲染

// app/dashboard/page.tsx
import { Suspense } from 'react'

export default function Dashboard() {
  return (
    <div>
      <Suspense fallback={<StatsSkeletion />}>
        <UserStats /> {/* 異步組件 */}
      </Suspense>

      <Suspense fallback={<CardsSkeletion />}>
        <RecentFlashcards /> {/* 異步組件 */}
      </Suspense>

      <Suspense fallback={<ProgressSkeletion />}>
        <LearningProgress /> {/* 異步組件 */}
      </Suspense>
    </div>
  )
}

🗄️ 數據獲取優化

1. 數據預載入

// app/flashcards/[id]/page.tsx
import { preloadFlashcard } from '@/lib/api/flashcards'

export default async function FlashcardPage({ params }: { params: { id: string } }) {
  // 預載入相關數據
  preloadFlashcard(params.id)
  preloadRelatedFlashcards(params.id)

  const flashcard = await getFlashcard(params.id)

  return <FlashcardDetail flashcard={flashcard} />
}

2. 並行數據獲取

// hooks/useParallelFetch.ts
export function useDashboardData() {
  const [stats, flashcards, progress] = useQueries({
    queries: [
      {
        queryKey: ['stats'],
        queryFn: fetchUserStats,
        staleTime: 5 * 60 * 1000, // 5 分鐘
      },
      {
        queryKey: ['recent-flashcards'],
        queryFn: fetchRecentFlashcards,
        staleTime: 60 * 1000, // 1 分鐘
      },
      {
        queryKey: ['progress'],
        queryFn: fetchLearningProgress,
        staleTime: 10 * 60 * 1000, // 10 分鐘
      },
    ],
  })

  return {
    stats: stats.data,
    flashcards: flashcards.data,
    progress: progress.data,
    isLoading: stats.isLoading || flashcards.isLoading || progress.isLoading,
  }
}

3. 無限滾動優化

// hooks/useInfiniteFlashcards.ts
export function useInfiniteFlashcards() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['flashcards'],
    queryFn: ({ pageParam = 0 }) => fetchFlashcards({ page: pageParam, limit: 20 }),
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
    staleTime: 5 * 60 * 1000,
    gcTime: 10 * 60 * 1000,
  })

  const allFlashcards = useMemo(
    () => data?.pages.flatMap(page => page.items) ?? [],
    [data]
  )

  return {
    flashcards: allFlashcards,
    loadMore: fetchNextPage,
    hasMore: hasNextPage,
    isLoading: isFetchingNextPage,
  }
}

🎨 CSS 優化

1. Critical CSS

// pages/_document.tsx
import { getCssText } from '@/stitches.config'

export default function Document() {
  return (
    <Html>
      <Head>
        <style
          id="stitches"
          dangerouslySetInnerHTML={{ __html: getCssText() }}
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}

2. CSS-in-JS 優化

// 使用 CSS Variables 減少運行時計算
const theme = {
  colors: {
    primary: 'var(--color-primary)',
    secondary: 'var(--color-secondary)',
  },
}

// 避免動態樣式
// ❌ 錯誤
const Button = styled.button`
  background: ${props => props.primary ? 'blue' : 'gray'};
`

// ✅ 正確
const Button = styled.button`
  &[data-variant="primary"] {
    background: var(--color-primary);
  }
  &[data-variant="secondary"] {
    background: var(--color-secondary);
  }
`

📊 監控與分析

1. Web Vitals 監控

// app/layout.tsx
import { WebVitalsReporter } from '@/components/WebVitalsReporter'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <WebVitalsReporter />
      </body>
    </html>
  )
}

// components/WebVitalsReporter.tsx
'use client'

import { useReportWebVitals } from 'next/web-vitals'

export function WebVitalsReporter() {
  useReportWebVitals((metric) => {
    // 發送到分析服務
    if (metric.label === 'web-vital') {
      console.log(metric)

      // 發送到 Google Analytics
      if (typeof window !== 'undefined' && window.gtag) {
        window.gtag('event', metric.name, {
          value: Math.round(metric.value),
          event_label: metric.id,
          non_interaction: true,
        })
      }
    }
  })

  return null
}

2. Bundle 分析

// package.json
{
  "scripts": {
    "analyze": "ANALYZE=true next build",
    "analyze:server": "BUNDLE_ANALYZE=server next build",
    "analyze:browser": "BUNDLE_ANALYZE=browser next build"
  }
}
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})

module.exports = withBundleAnalyzer({
  // 其他配置
})

🔧 性能優化檢查清單

開發階段

  • 使用 React DevTools Profiler 分析渲染
  • 檢查不必要的重新渲染
  • 實施代碼分割
  • 優化圖片載入
  • 使用適當的快取策略

構建優化

  • 啟用生產模式構建
  • 壓縮 JavaScript 和 CSS
  • 移除未使用的代碼
  • 優化字體載入
  • 啟用 Brotli/Gzip 壓縮

運行時優化

  • 實施延遲載入
  • 使用 Service Worker 快取
  • 優化第三方腳本載入
  • 減少主線程工作
  • 優化數據庫查詢

監控

  • 設置 Real User Monitoring (RUM)
  • 追蹤 Core Web Vitals
  • 設置性能預算
  • 定期進行 Lighthouse 審計
  • 監控 JavaScript 錯誤率

📈 性能預算

// performance-budget.js
module.exports = {
  bundles: [
    {
      name: 'main',
      maxSize: '150kb',
    },
    {
      name: 'vendor',
      maxSize: '250kb',
    },
  ],
  metrics: {
    fcp: 1800,
    lcp: 2500,
    fid: 100,
    cls: 0.1,
    tti: 3800,
  },
}

🚀 快速優化清單

立即可做

  1. 啟用 Next.js Image 組件
  2. 添加 loading="lazy" 到圖片
  3. 預連接到外部域名
  4. 內聯關鍵 CSS
  5. 延遲非關鍵 JavaScript

短期改進

  1. 實施虛擬滾動
  2. 優化字體載入策略
  3. 使用 Web Workers
  4. 實施預載入策略
  5. 優化動畫性能

長期優化

  1. 實施 Edge Functions
  2. 使用 ISR (增量靜態再生)
  3. 優化資料庫索引
  4. 實施 CDN 策略
  5. 考慮使用 WebAssembly