555 lines
12 KiB
Markdown
555 lines
12 KiB
Markdown
# 性能優化指南
|
|
|
|
## 🚀 性能目標
|
|
|
|
| 指標 | 目標 | 工具 |
|
|
|------|------|------|
|
|
| **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. 代碼分割策略
|
|
|
|
```typescript
|
|
// 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. 動態導入
|
|
|
|
```typescript
|
|
// 使用動態導入減少初始載入
|
|
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
|
|
|
|
```typescript
|
|
// utils/index.ts
|
|
// ❌ 錯誤:導出所有
|
|
export * from './helpers'
|
|
|
|
// ✅ 正確:具名導出
|
|
export { formatDate, parseJSON } from './helpers'
|
|
|
|
// 使用時
|
|
// ❌ 錯誤:導入整個庫
|
|
import * as utils from '@/utils'
|
|
|
|
// ✅ 正確:只導入需要的
|
|
import { formatDate } from '@/utils'
|
|
```
|
|
|
|
## 🖼️ 圖片優化
|
|
|
|
### 1. Next.js Image 優化
|
|
|
|
```typescript
|
|
// 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. 響應式圖片
|
|
|
|
```typescript
|
|
// 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 組件優化
|
|
|
|
```typescript
|
|
// 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. 虛擬滾動
|
|
|
|
```typescript
|
|
// 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 與並行渲染
|
|
|
|
```typescript
|
|
// 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. 數據預載入
|
|
|
|
```typescript
|
|
// 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. 並行數據獲取
|
|
|
|
```typescript
|
|
// 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. 無限滾動優化
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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 優化
|
|
|
|
```typescript
|
|
// 使用 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 監控
|
|
|
|
```typescript
|
|
// 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 分析
|
|
|
|
```json
|
|
// package.json
|
|
{
|
|
"scripts": {
|
|
"analyze": "ANALYZE=true next build",
|
|
"analyze:server": "BUNDLE_ANALYZE=server next build",
|
|
"analyze:browser": "BUNDLE_ANALYZE=browser next build"
|
|
}
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// 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 錯誤率
|
|
|
|
## 📈 性能預算
|
|
|
|
```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 |