效能:先量測,再最佳化
該看哪三個指標、首屏預算怎麼訂、常見的自傷行為,以及不要提前最佳化的具體理由。
1. 三個指標
| 指標 | 意思 | 目標 |
|---|---|---|
| LCP | 最大內容繪製 —— 主視覺或標題出現的時間 | < 2.5s |
| INP | 互動到下一次繪製 —— 點下去多久才有反應 | < 200ms |
| CLS | 累積版面位移 —— 東西會不會亂跳 | < 0.1 |
其他數字(bundle 大小、請求數)是手段,這三個才是使用者的體感。
量測:Lighthouse 看實驗室數據,真實使用者資料用 web-vitals 套件回報。開發機上的數字沒有意義 —— 你的網路太快、裝置太好。
2. 首屏預算
| 項目 | 上限 |
|---|---|
| JS(gzip) | 120KB |
| CSS | 40KB |
| 圖片 | 250KB |
| 字型 | 2 個家族,各 ≤ 2 字重 |
| 第三方腳本 | 越少越好,能不要就不要 |
超過要有人簽字。沒有預算的效能討論會變成「感覺有點慢」然後不了了之。
3. 改善 LCP
- 首屏圖片不要 lazy,加
fetchpriority="high" - 字型加 preconnect,用
display: swap - 首屏內容用 SSR,不要等 JS 才渲染
- 關鍵 CSS 內聯或提早載入
- 不要讓第三方腳本擋住渲染(
async/defer)
最常見的 LCP 殺手是「首屏主視覺加了 loading="lazy"」—— 出於好意,效果相反。
4. 改善 INP
- 把長任務切開(
scheduler.yield()或setTimeout 0) - 捲動與滑鼠監聽用
{ passive: true } - 高頻更新(串流輸出、拖曳)用
requestAnimationFrame節流 - 不要在事件處理裡做同步的大量計算
export function rafThrottle<T extends (...args: never[]) => void>(fn: T) {
let frame: number | null = null;
let lastArgs: Parameters<T> | null = null;
return (...args: Parameters<T>) => {
lastArgs = args;
if (frame !== null) return;
frame = requestAnimationFrame(() => {
frame = null;
if (lastArgs) fn(...lastArgs);
});
};
}
模型每秒可以吐幾百個 token,但螢幕一秒只更新 60 次。不節流的話,你在為沒人看得到的畫面燒 CPU。
5. 改善 CLS
見 layout-and-overlap 第 6 節。摘要:
- 圖片給尺寸
- 字型用接近的 fallback
- 骨架屏尺寸要對
scrollbar-gutter: stable- 動態插入的東西要預留空間
6. 減少 JS
先刪,再優化。
npx vite-bundle-visualizer # 看誰最大
npx knip # 找沒用到的檔案與依賴
npx bundle-phobia <package> # 加之前先看大小
常見的肥肉:
| 套件 | 大小 | 替代 |
|---|---|---|
| moment | ~70KB | Intl.DateTimeFormat(0KB) |
| lodash(整包) | ~70KB | 只 import 需要的,或自己寫五行 |
| axios | ~15KB | fetch(0KB) |
| 圖示套件(整包) | 100KB+ | 只 inline 你用到的那幾個 SVG |
| UI 套件 | 100KB+ | 這套設計系統就是純 CSS |
加一個依賴之前先問:這五行我自己寫要多久?
7. 不要提前最佳化
// 沒量測就加 memo,只是讓程式碼變難讀
const value = useMemo(() => a + b, [a, b]);
const handler = useCallback(() => setOpen(true), []);
useMemo / useCallback 本身也有成本(比較依賴、佔記憶體)。對於便宜的計算,它比直接算還慢。
加 memo 的正當理由只有一個:你用 profiler 量到它慢。
同樣的道理適用於:虛擬捲動(先確認清單真的長)、Web Worker(先確認計算真的重)、快取層(先確認真的重複算)。
8. Cloudflare Workers 的特性
- 冷啟動幾乎為零,不需要為此設計
- CPU 時間有限(免費方案 10ms,付費 30s),重運算要丟去別的地方
- 靜態資產走 assets binding,不進 worker,最快
- 能預先產生就預先產生:本站的
/themes.css是純計算的結果,所以 build 時就變成靜態檔,執行期零成本
9. 量測流程
- 先在節流的網路與 CPU 下錄一次(DevTools: Slow 4G + 4x CPU slowdown)
- 找出最大的那一個問題
- 只修那一個
- 再量一次
- 重複
一次改五個地方,你不會知道哪一個有效。
10. 檢查清單
- 有量過 LCP / INP / CLS,不是憑感覺
- 首屏圖片沒有 lazy,有
fetchpriority="high" - 首屏 JS < 120KB gzip
- 沒有 moment、沒有整包 lodash、沒有整包圖示庫
- 高頻更新有節流
- 捲動監聽是 passive
- memo 都是量測後才加的
- 能預先產生的東西沒有留在執行期算