performance-optimization

performance-optimization

熱門

最佳化前端、後端、查詢與資料庫的應用程式效能。當有效能需求、懷疑效能回歸、需要改善 Core Web Vitals 或載入時間、需要修正 N+1 查詢模式,或效能分析發現瓶頸時使用。

7.7萬星標
0分支
更新於 2026/7/3
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.memouseMemo(過度使用和不足使用一樣糟糕)

驗證

在進行任何與效能相關的變更後:

  • [ ] 存在變更前後的測量數據(具體數字)
  • [ ] 已識別並處理特定瓶頸
  • [ ] Core Web Vitals 在「良好」門檻內
  • [ ] 套件大小沒有顯著增加
  • [ ] 新的資料擷取程式碼中沒有 N+1 查詢
  • [ ] 效能預算在 CI 中通過(如有設定)
  • [ ] 現有測試仍然通過(最佳化沒有破壞行為)