SKILL.md
readonlyread-only
name
performance-optimization
description
最佳化前端、後端、查詢與資料庫的應用程式效能。當有效能需求、懷疑效能回歸、需要改善 Core Web Vitals 或載入時間、需要修正 N+1 查詢模式,或效能分析發現瓶頸時使用。
效能最佳化
概述
先測量再最佳化。沒有測量的效能工作只是猜測——而猜測會導致過早最佳化,增加複雜性卻沒有改善真正重要的事。先進行效能分析,找出實際瓶頸,修復它,再測量一次。只最佳化那些經測量證明重要的部分。
使用時機
- 規格中有效能要求(載入時間預算、回應時間 SLA)
- 使用者或監控回報速度緩慢
- Core Web Vitals 分數低於門檻
- 懷疑某項變更引入了效能回歸
- 正在開發處理大量資料或高流量的功能
何時不該使用: 在沒有證據顯示問題之前,不要進行最佳化。過早最佳化會增加複雜性,其成本超過所獲得的效能提升。
Core Web Vitals 目標
| 指標 | 良好 | 待改善 | 差 |
|---|---|---|---|
| LCP (最大內容繪製) | ≤ 2.5s | ≤ 4.0s | > 4.0s |
| INP (與下一次繪製的互動) | ≤ 200ms | ≤ 500ms | > 500ms |
| CLS (累計版面配置位移) | ≤ 0.1 | ≤ 0.25 | > 0.25 |
最佳化工作流程
1. 測量 → 使用真實資料建立基準
2. 識別 → 找出實際瓶頸(而非假設的)
3. 修復 → 針對特定瓶頸處理
4. 驗證 → 再次測量,確認改善
5. 防護 → 加入監控或測試以防止回歸
步驟 1:測量
兩種互補的方法——兩者都使用:
- 合成測試(Lighthouse、DevTools 效能面板): 受控環境,可重現。最適合 CI 回歸檢測和隔離特定問題。
- 真實使用者監控(web-vitals 函式庫、CrUX): 真實條件下的真實使用者資料。需要驗證修復是否確實改善了使用者體驗。
前端:
# 合成測試:Chrome DevTools 中的 Lighthouse(或 CI)
# Chrome DevTools → 效能面板 → 錄製
# Chrome DevTools MCP → 效能追蹤
# 真實使用者監控:程式碼中的 Web Vitals 函式庫
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(console.log);
onINP(console.log);
onCLS(console.log);
後端:
# 回應時間記錄
# 應用程式效能監控(APM)
# 資料庫查詢記錄(含時間)
# 簡單計時
console.time('db-query');
const result = await db.query(...);
console.timeEnd('db-query');
從哪裡開始測量
根據症狀決定先測量什麼:
什麼變慢了?
├── 首次頁面載入
│ ├── 套件過大? --> 測量套件大小,檢查程式碼分割
│ ├── 伺服器回應慢? --> 在 DevTools 網路瀑布圖中測量 TTFB
│ │ ├── DNS 時間長? --> 為已知來源加入 dns-prefetch / preconnect
│ │ ├── TCP/TLS 時間長? --> 啟用 HTTP/2,檢查邊緣部署,keep-alive
│ │ └── 等待(伺服器)時間長? --> 分析後端,檢查查詢和快取
│ └── 渲染阻塞資源? --> 檢查網路瀑布圖中的 CSS/JS 阻塞
├── 互動感覺遲鈍
│ ├── 點擊時 UI 凍結? --> 分析主執行緒,尋找長時間任務(>50ms)
│ ├── 表單輸入延遲? --> 檢查重新渲染、受控元件的開銷
│ └── 動畫卡頓? --> 檢查佈局震盪、強制回流
├── 導航後的頁面
│ ├── 資料載入? --> 測量 API 回應時間,檢查瀑布效應
│ └── 客戶端渲染? --> 分析元件渲染時間,檢查 N+1 請求
└── 後端 / API
├── 單一端點慢? --> 分析資料庫查詢,檢查索引
├── 所有端點都慢? --> 檢查連線池、記憶體、CPU
└── 間歇性變慢? --> 檢查鎖競爭、GC 暫停、外部依賴
步驟 2:識別瓶頸
各類別的常見瓶頸:
前端:
| 症狀 | 可能原因 | 調查方式 |
|---|---|---|
| LCP 慢 | 大型圖片、渲染阻塞資源、伺服器慢 | 檢查網路瀑布圖、圖片大小 |
| CLS 高 | 未設定尺寸的圖片、延遲載入內容、字型位移 | 檢查版面配置位移歸因 |
| INP 差 | 主執行緒上的大量 JavaScript、大型 DOM 更新 | 檢查效能追蹤中的長時間任務 |
| 初始載入慢 | 大型套件、大量網路請求 | 檢查套件大小、程式碼分割 |
後端:
| 症狀 | 可能原因 | 調查方式 |
|---|---|---|
| API 回應慢 | N+1 查詢、缺少索引、未最佳化的查詢 | 檢查資料庫查詢日誌 |
| 記憶體增長 | 洩漏的參考、無限制的快取、大型負載 | 堆積快照分析 |
| CPU 飆升 | 同步大量計算、正規表達式回溯 | CPU 分析 |
| 高延遲 | 缺少快取、重複計算、網路跳躍 | 追蹤請求在堆疊中的路徑 |
步驟 3:修復常見反模式
N+1 查詢(後端)
// 錯誤:N+1 — 每個任務查詢一次擁有者
const tasks = await db.tasks.findMany();
for (const task of tasks) {
task.owner = await db.users.findUnique({ where: { id: task.ownerId } });
}
// 正確:使用 join/include 的單一查詢
const tasks = await db.tasks.findMany({
include: { owner: true },
});
無限制的資料擷取
// 錯誤:擷取所有記錄
const allTasks = await db.tasks.findMany();
// 正確:分頁並限制數量
const tasks = await db.tasks.findMany({
take: 20,
skip: (page - 1) * 20,
orderBy: { createdAt: 'desc' },
});
缺少圖片最佳化(前端)
<!-- 錯誤:沒有尺寸,沒有格式最佳化 -->
<img src="/hero.jpg" />
<!-- 正確:Hero / LCP 圖片 — 藝術指導 + 解析度切換,高優先權 -->
<!--
結合兩種技術:
- 藝術指導 (media):每個斷點不同的裁切/構圖
- 解析度切換 (srcset + sizes):根據螢幕密度提供正確檔案大小
-->
<picture>
<!-- 手機:直式裁切 (8:10) -->
<source
media="(max-width: 767px)"
srcset="/hero-mobile-400.avif 400w, /hero-mobile-800.avif 800w"
sizes="100vw"
width="800"
height="1000"
type="image/avif"
/>
<source
media="(max-width: 767px)"
srcset="/hero-mobile-400.webp 400w, /hero-mobile-800.webp 800w"
sizes="100vw"
width="800"
height="1000"
type="image/webp"
/>
<!-- 桌機:橫式裁切 (2:1) -->
<source
srcset="/hero-800.avif 800w, /hero-1200.avif 1200w, /hero-1600.avif 1600w"
sizes="(max-width: 1200px) 100vw, 1200px"
width="1200"
height="600"
type="image/avif"
/>
<source
srcset="/hero-800.webp 800w, /hero-1200.webp 1200w, /hero-1600.webp 1600w"
sizes="(max-width: 1200px) 100vw, 1200px"
width="1200"
height="600"
type="image/webp"
/>
<img
src="/hero-desktop.jpg"
width="1200"
height="600"
fetchpriority="high"
alt="Hero 圖片描述"
/>
</picture>
<!-- 正確:折疊線以下的圖片 — 延遲載入 + 非同步解碼 -->
<img
src="/content.webp"
width="800"
height="400"
loading="lazy"
decoding="async"
alt="內容圖片描述"
/>
不必要的重新渲染(React)
// 錯誤:每次渲染都建立新物件,導致子元件重新渲染
function TaskList() {
return <TaskFilters options={{ sortBy: 'date', order: 'desc' }} />;
}
// 正確:穩定的參考
const DEFAULT_OPTIONS = { sortBy: 'date', order: 'desc' } as const;
function TaskList() {
return <TaskFilters options={DEFAULT_OPTIONS} />;
}
// 對昂貴的元件使用 React.memo
const TaskItem = React.memo(function TaskItem({ task }: Props) {
return <div>{/* 昂貴的渲染 */}</div>;
});
// 對昂貴的計算使用 useMemo
function TaskStats({ tasks }: Props) {
const stats = useMemo(() => calculateStats(tasks), [tasks]);
return <div>{stats.completed} / {stats.total}</div>;
}
大型套件大小
// 現代打包工具(Vite、webpack 5+)會自動處理具名匯入的 tree-shaking,
// 前提是依賴項提供 ESM 並在 package.json 中標記 `sideEffects: false`。
// 在改變匯入風格前先進行分析——真正的效益來自於分割和延遲載入。
// 正確:對大型、不常用功能使用動態匯入
const ChartLibrary = lazy(() => import('./ChartLibrary'));
// 正確:包裝在 Suspense 中的路由層級程式碼分割
const SettingsPage = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<SettingsPage />
</Suspense>
);
}
缺少快取(後端)
// 快取頻繁讀取、很少變更的資料
const CACHE_TTL = 5 * 60 * 1000; // 5 分鐘
let cachedConfig: AppConfig | null = null;
let cacheExpiry = 0;
async function getAppConfig(): Promise<AppConfig> {
if (cachedConfig && Date.now() < cacheExpiry) {
return cachedConfig;
}
cachedConfig = await db.config.findFirst();
cacheExpiry = Date.now() + CACHE_TTL;
return cachedConfig;
}
// 靜態資源的 HTTP 快取標頭
app.use('/static', express.static('public', {
maxAge: '1y', // 快取 1 年
immutable: true, // 永不重新驗證(在檔案名稱中使用內容雜湊)
}));
// API 回應的 Cache-Control
res.set('Cache-Control', 'public, max-age=300'); // 5 分鐘
效能預算
設定預算並強制執行:
JavaScript 套件:< 200KB gzipped(初始載入)
CSS:< 50KB gzipped
圖片:< 200KB 每張(折疊線以上)
字型:< 100KB 總計
API 回應時間:< 200ms(p95)
可互動時間:< 3.5s(4G 網路)
Lighthouse 效能分數:≥ 90
在 CI 中強制執行:
# 套件大小檢查
npx bundlesize --config bundlesize.config.json
# Lighthouse CI
npx lhci autorun
另請參閱
如需詳細的效能檢查清單、最佳化命令和反模式參考,請參閱 references/performance-checklist.md。
常見的合理化藉口
| 合理化藉口 | 現實 |
|---|---|
| "我們之後再最佳化" | 效能債會累積。立即修復明顯的反模式,將微最佳化延後。 |
| "在我的機器上很快" | 你的機器不是使用者的機器。在代表性的硬體和網路上進行分析。 |
| "這個最佳化很明顯" | 如果你沒有測量,你就不確定。先分析。 |
| "使用者不會注意到 100ms" | 研究顯示 100ms 的延遲會影響轉換率。使用者比你認為的更敏感。 |
| "框架會處理效能" | 框架可以預防一些問題,但無法修復 N+1 查詢或過大的套件。 |
紅旗
- 沒有分析資料支持的最佳化
- 資料擷取中的 N+1 查詢模式
- 沒有分頁的清單端點
- 沒有尺寸、延遲載入或響應式尺寸的圖片
- 套件大小在沒有審查的情況下增長
- 生產環境中沒有效能監控
- 到處使用
React.memo和useMemo(過度使用和不足使用一樣糟糕)
驗證
在進行任何與效能相關的變更後:
- [ ] 存在變更前後的測量數據(具體數字)
- [ ] 已識別並處理特定瓶頸
- [ ] Core Web Vitals 在「良好」門檻內
- [ ] 套件大小沒有顯著增加
- [ ] 新的資料擷取程式碼中沒有 N+1 查詢
- [ ] 效能預算在 CI 中通過(如有設定)
- [ ] 現有測試仍然通過(最佳化沒有破壞行為)






