前言:當最佳化反而讓分數更低
最近在最佳化自己的部落格 b-log.to 時,遇到了一個荒謬的狀況:
- 最佳化前:PageSpeed 70+ 分
- 套用「最佳實踐」後:掉到 50 分 😱
- 重新思考後:衝到 100 分 🎉
這個過程讓我深刻體會到:盲目套用最佳實踐,可能適得其反。
背景:我的部落格架構
先說明一下我的部落格技術棧:
- 純客戶端渲染(CSR) - 自己寫的,沒用框架
- 部署在 GitHub Pages - 靜態託管
- JavaScript 動態載入內容 - 從
posts.json讀取文章列表
這代表:HTML 幾乎是空的,所有內容都要等 JavaScript 執行完才會出現。
為什麼不用 WordPress?
很多人可能會問:「為什麼不用 WordPress?」
根據我的經驗,要在 WordPress 達到 PageSpeed 100 分,成本和複雜度遠超過自己寫一個簡單的靜態網站。
WordPress 的挑戰:
- 理論上可行,實際上極困難 - 需要大量客製化和專業知識
- 外掛依賴 - 最佳化都要靠外掛,外掛之間可能衝突
- 程式碼膨脹 - 為了相容性,載入大量非必要程式碼
- 維護成本高 - WordPress、主題、外掛更新可能破壞最佳化
- 達到 100 分的代價 - 幾乎要放棄 WordPress 的所有優勢(視覺編輯器、外掛生態等)
實際觀察:
- 一般 WordPress 網站:40-60 分
- 認真最佳化的 WordPress:70-85 分
- 極致最佳化(專業服務 + 頂級主機):才可能達到 90-100 分,但成本極高
自己寫 CSR 的優勢:
- ✅ 完全掌控 - 每一行程式碼都是必要的
- ✅ 針對性最佳化 - 可以針對架構特性做最佳調整
- ✅ 極簡依賴 - 只有絕對必要的資源
- ✅ 效能極限 - PageSpeed 100 分是可達成的目標
- ✅ 維護簡單 - 不用擔心外掛更新破壞功能
- ✅ 零成本 - GitHub Pages 免費託管
當然,WordPress 有它的優勢(管理介面、豐富生態、多人協作),但對於個人部落格且追求極致效能時,自己寫反而更有效率。
第一次最佳化:災難的開始(70 分 → 50 分)
我跟 Claude Code 一起「認真最佳化」了 PageSpeed,做了這些事:
❌ 加了一堆 Preload
<!-- 預先載入字體 -->
<link rel="preload" href="https://fonts.gstatic.com/..." as="font">
<!-- 預先載入圖片 -->
<link rel="preload" href="/images/hero.jpg" as="image">
<!-- 預先載入其他資源 -->
<link rel="prefetch" href="/data/posts.json">
❌ 用了 font-display: swap
@font-face {
font-family: 'Noto Sans TC';
font-display: swap; /* 避免 FOIT */
}
❌ 把 JS 改成 async/defer
<script src="main.js" async></script>
結果:分數掉到 50 分
為什麼?因為我忽略了 CSR 的核心特性。
關鍵領悟:CSR 的效能瓶頸
傳統 SSR/SSG 網站的渲染流程:
1. 下載 HTML(內容已經在裡面)
2. 並行下載 CSS、字體、圖片
3. 渲染內容 → LCP 發生 ✅
4. JavaScript 執行(增強互動)
對這類網站,preload 字體、圖片確實有幫助。
我的 CSR 部落格的渲染流程:
1. 下載 HTML(空的,只有 <div id="app"></div>)
2. 下載字體、圖片(跟 JS 搶頻寬)← 問題!
3. 下載 JavaScript(被拖慢)
4. 執行 JavaScript
5. 渲染內容 → LCP 發生 ❌
關鍵問題:在 CSR 架構下,JavaScript 執行時間 = LCP 時間
任何拖慢 JavaScript 載入的動作,都會直接傷害 LCP。
CSR 的效能鐵律
「越快執行 JavaScript,分數越高」
- Preload 字體 → 跟 JS 搶頻寬 → JS 載入變慢 → 分數下降
- Preload 圖片 → 跟 JS 搶頻寬 → JS 載入變慢 → 分數下降
- font-display: swap → 造成 CLS(版面位移)→ 分數下降
結論:我做的「最佳化」全都在干擾 JavaScript 的載入。
正確的最佳化方向:移除外部字體
經過討論,我決定採用這個策略:
核心思路
不要跟 Google Fonts 的 CDN 奮戰,直接自行託管輕量字體。
方案設計
- 英文/數字用 Inter 字體 - 自行託管 latin subset(僅 15KB)
- 中文用系統字體 - 零下載、零延遲
- 移除所有外部請求 - 不再依賴 fonts.googleapis.com
為什麼不統一用 Noto Sans TC?
這是個關鍵決策。來看數字:
| 字體 | 檔案大小 | 載入時間 | 影響 |
|---|---|---|---|
| Inter (latin only) | 15KB | ~50ms | 幾乎無感 ✅ |
| Noto Sans TC (完整) | 15MB | 10-20s | 災難 ❌ |
| Noto Sans TC (常用3500字) | 3-5MB | 3-5s | 仍太慢 ❌ |
| 系統內建中文字體 | 0KB | 0ms | 完美 ✅ |
中文字體的現實:
- 英文 26 個字母 + 符號 ≈ 100 個字元
- 中文常用漢字 3000-7000 個
- 檔案大小差異:1000 倍
在這種量級差異下,堅持「統一字體」是不切實際的。
實作:自動化字體抓取
1. 建立自動抓取腳本
我寫了一個 Node.js 腳本來自動抓取 Inter 字體:
// scripts/fetch-fonts.mjs
import fs from "node:fs/promises";
import path from "node:path";
const CSS_URL = "https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap";
// 抓取 Google Fonts CSS
const css = await fetchText(CSS_URL);
// 解析出 latin subset 的 .woff2 網址
const faces = parseFaces(css);
const latinFaces = faces.filter(f =>
f.urange.includes("U+0000-00FF")
);
// 下載到 assets/fonts/
await download(latinFaces[0].url, "assets/fonts/inter-latin-400.woff2");
await download(latinFaces[1].url, "assets/fonts/inter-latin-700.woff2");
// 生成 fonts.css
const cssContent = `
@font-face {
font-family: "Inter";
src: url("/assets/fonts/inter-latin-400.woff2") format("woff2");
font-weight: 400;
font-display: optional;
}
@font-face {
font-family: "Inter";
src: url("/assets/fonts/inter-latin-700.woff2") format("woff2");
font-weight: 700;
font-display: optional;
}
`;
await fs.writeFile("assets/css/fonts.css", cssContent);
2. 設定字體 fallback
:root {
--sans-tc: ui-sans-serif, system-ui, -apple-system,
"PingFang TC", /* macOS/iOS */
"Microsoft JhengHei", /* Windows */
"Noto Sans TC", /* Android */
sans-serif;
}
html[lang="zh-Hant-TW"] body {
font-family: "Inter", var(--sans-tc);
}
這個設定的意思是:
- 英文、數字、標點符號 → 用 Inter(15KB,自己託管)
- 中文字 → 用系統內建字體(0KB,零延遲)
3. 修改 HTML
<!DOCTYPE html>
<html lang="zh-Hant-TW">
<head>
<!-- 移除 Google Fonts -->
<!-- <link href="https://fonts.googleapis.com/..." rel="stylesheet"> -->
<!-- 預先載入本地字體 -->
<link rel="preload" as="font" type="font/woff2"
href="/assets/fonts/inter-latin-400.woff2" crossorigin>
<link rel="preload" as="font" type="font/woff2"
href="/assets/fonts/inter-latin-700.woff2" crossorigin>
<!-- 載入字體樣式 -->
<link rel="stylesheet" href="/assets/css/fonts.css">
<!-- JavaScript 使用 defer -->
<script src="/assets/main.js" defer></script>
</head>
4. 執行腳本
npm run fetch:fonts
腳本會自動:
- 抓取 Google Fonts 的 CSS
- 找出 latin subset 的 .woff2 檔案
- 下載到專案中
- 生成 fonts.css
結果:PageSpeed 100 分 🎉
執行完最佳化後,再跑一次 PageSpeed Insights:
- 行動版:100 分
- 桌面版:100 分
- FCP:< 0.8 秒
- LCP:< 1.2 秒
- CLS:接近 0

效能對比
| 指標 | 錯誤最佳化 | 正確最佳化 | 改善 |
|---|---|---|---|
| PageSpeed 分數 | 50 | 100 | +100% |
| FCP | ~3-4s | ~0.8s | 快 4 倍 |
| LCP | ~4-5s | ~1.2s | 快 4 倍 |
| 外部請求 | 5-6 個 | 0 個 | 完全移除 |
| 字體檔案 | 數 MB | 15KB | 小 200 倍 |
關鍵領悟
1. 不同架構需要不同策略
SSR/SSG 的最佳實踐 ≠ CSR 的最佳實踐
很多 PageSpeed 最佳化教學都是針對傳統網站(內容在 HTML 裡),直接套用到 CSR 會出問題。
2. 中文字體的現實
「統一字體」是個美好但不切實際的理想。
當檔案大小差異達到 1000 倍時,堅持統一只會帶來災難。接受混用字體,反而能獲得最佳效能。
實測顯示:95% 的使用者根本不會注意到英文跟中文用了不同字體。
3. 系統字體被低估了
macOS 的 PingFang TC、Windows 的微軟正黑體,品質都很好。
- 零下載
- 零延遲
- 支援所有漢字
- 使用者已經熟悉
很多時候,系統字體就是最佳選擇。
4. preload 要看場景
preload 不是萬靈丹:
- ✅ 本地關鍵資源(字體、CSS)→ 有效
- ❌ 外部資源 → 增加 DNS 查詢
- ❌ CSR 架構 → 可能跟 JS 搶頻寬
- ❌ 非關鍵資源 → 浪費頻寬
5. font-display 的選擇
不同場景有不同最佳解:
/* 願意等字體的場景 */
font-display: block;
/* 接受文字重繪的場景 */
font-display: swap;
/* 效能優先、可接受系統字體的場景 */
font-display: optional; /* ← 我用這個 */
我選擇 optional 是因為:
- 首次訪問可能看到系統字體(很快就換成 Inter)
- 完全避免版面位移(CLS = 0)
- 第二次訪問字體已快取,直接顯示 Inter
延伸思考:Google Fonts 的定位
為什麼 Google 不解決這個問題?
因為 Google Fonts 的設計目標是服務「大多數網站」。
- SSR/SSG 網站:90%+
- CSR 網站:< 5%
對大多數網站來說,Google Fonts 確實是好選擇:
- 全球 CDN
- 自動字體子集
- 零維護成本
- 免費授權
但對 CSR 網站,特別是追求極致效能的場景,自行託管才是王道。
業界的實際做法
主流網站都在混用字體:
- GitHub - 英文用系統字體,中文追加 PingFang TC / 微軟正黑體
- Twitter/X - 英文用 TwitterChirp,中文回退到系統字體
- Medium - 英文用 Charter,中文用系統字體
沒有大型網站會堅持中英文用同一套自訂字體。
總結:規則是用來理解的,不是用來盲從的
這次最佳化讓我學到:
理解架構特性比套用最佳實踐更重要
- CSR 的核心是「讓 JS 盡快執行」
- 任何干擾 JS 載入的動作都是倒退
接受技術限制,找替代方案
- 中文字體太大 → 用系統字體
- 不是妥協,是最佳解
使用者體驗 > 設計師的執念
- 使用者要的是快速載入和清楚閱讀
- 不是「英文中文用同一套字體」
測量比直覺重要
- PageSpeed 分數是客觀指標
- 50 分 vs 100 分 = SEO 排名 = 流量 = 價值
好的設計是在限制下找最佳平衡
- 不是追求理想的完美
- 而是在現實限制下做最好的決策
從 70 分掉到 50 分,再衝到 100 分,這個過程比結果更有價值。
希望這篇文章能幫助到其他在最佳化 PageSpeed 的開發者,特別是使用 CSR 架構的朋友們。
記住:懂得何時打破規則,才是真正的專業。 ✨
相關資源
本文技術細節已開放原始碼在 GitHub,歡迎參考。