12 KiB
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,
},
}
🚀 快速優化清單
立即可做
- 啟用 Next.js Image 組件
- 添加 loading="lazy" 到圖片
- 預連接到外部域名
- 內聯關鍵 CSS
- 延遲非關鍵 JavaScript
短期改進
- 實施虛擬滾動
- 優化字體載入策略
- 使用 Web Workers
- 實施預載入策略
- 優化動畫性能
長期優化
- 實施 Edge Functions
- 使用 ISR (增量靜態再生)
- 優化資料庫索引
- 實施 CDN 策略
- 考慮使用 WebAssembly