先把尺寸搞清楚

∑ Calc 是一個 5,780 行的 index.html,GitHub Pages 送 gzip,透過 Fastly CDN。 先量網路傳輸大小:index.html 66KB(gzip),5 個 js/*.js 合計 14KB(gzip), sw.js 0.8KB,總計約 80KB。完全在 landing page 預算之內。

但傳輸大小不等於 parse 成本。把 index.html 結構切開來看: 第 1–50 行是 head(3KB); 第 51–1,627 行是 inline CSS(62KB / 1,577 行); 第 1,628–2,448 行是 HTML body(57KB / 821 行); 第 2,449–5,671 行是 inline JS(177KB / 3,223 行); 第 5,672–5,778 行是 modal 和 5 個外部 JS。

原本的推測被打臉:inline CSS 62KB 看起來很大,但 inline 反而沒有 external CSS 的 fetch 延遲, 不阻塞 render。真正影響 TTI 的是 177KB inline JS 的同步 parse 和 execute, 以及尾端 5 個外部 script 對 HTML parser 的 sync 阻塞。

defer 和 async 的差異在哪裡

兩者都讓 script 在下載時不阻塞 HTML parse——下載是並行的。差別在執行時機和順序。

async:下載完立即執行,不管 HTML 有沒有 parse 完,也不管其他 script 的順序。 幾個 async script 同時下載,誰先下載完誰先跑,順序是隨機的。

defer:下載和 HTML parse 並行(和 async 一樣快),但執行被延到整個 HTML parse 完成之後、 DOMContentLoaded 觸發之前。多個 defer script 按照在 HTML 裡出現的順序執行,順序固定。

∑ Calc 的 5 個 Pro JS 是強模組鏈: pro-ui.js 期待 pro-manager.js 已初始化, pro-manager.js 期待 pro-config.js 已設好 window.PRO_CONFIG。 用 async 的話,誰先跑誰就先找不到依賴——Pro 功能整個壞掉。 用 defer,文件順序就是執行順序,模組鏈不受影響。

和 inline JS 的互動

index.html 第 2,449–5,671 行是 177KB 的 inline JS,裡面有好幾處對 Pro 模組的引用, 例如第 3,035 行的注釋就寫了「pro-ui.js 與本檔載入有順序差,做 graceful retry」, 所有引用都用 window.X && ... 守衛加上 setTimeout retry。

加了 defer 之後,外部 JS 比 inline JS 晚執行(inline JS 不是 defer,HTML parse 時就同步跑)。 這個順序差在改動前就已存在,retry 機制也已到位, 不需要任何額外的程式碼變更就能正常運作。

Service Worker 的 cache-bust 不能省

∑ Calc 有一條鐵則:改 index.html 就必須 bump SW 的 CACHE_NAME。 SW 是 cache-first 策略,不 bump CACHE_NAME 的話,回訪用戶永遠看到舊版 index.html, 等於加 defer 對他們完全無效。

這次只動了 <script> 的屬性,5 個 js/*.js 的內容沒有變—— 但 index.html 變了,所以 sw.jsCACHE_NAMEsigma-calc-v3.8.1 bump 到 sigma-calc-v3.8.2js/*.js 的 query string 版本號不用動, 因為檔案內容沒有變。

本地驗證(5 項全綠)

python3 -m http.server 8765 起本地伺服器,過了以下幾個檢查點: SW CACHE_NAME 確認是 v3.8.2; 5 個 <script> 確認全帶 defer; window.PRO_CONFIGwindow.ProManagerwindow.LicenseAPIwindow.PayPalIntegrationwindow.ProUI 全部 object; ProManager.getTier() 回傳 free; 100 條公式渲染、41 個 🔒 鎖頭顯示; console.error = 0console.warn = 0; 截圖 UI 完整。

本地 domContentLoaded 16ms(localhost 沒有網路延遲,僅供確認無回歸)。 真正的效益要等上線後用 pagespeed.web.dev 量 CWV field data(含 CrUX 28 天), 才有意義的數字。理論預測:TTI / FCP 改善 50–200ms,依用戶網路速度而定。

這次的限制與下次要做的事

PSI 公開 API 今天 429(quota 耗盡),所以沒有線上 CWV baseline。 效能深審第二輪待辦: 用 pagespeed.web.dev 網頁版量 v3.8.1 vs v3.8.2 的 CWV 差異(含 CrUX); 掃 inline JS 177KB / 3,223 行的 dead code; 評估 AdSense 延遲載入的可行性; 5 個 js/*.js 合併為 1 個(少 4 次 HTTP 請求)的 ROI。

教訓

大頭不一定是看起來最大的那個:inline CSS 62KB,inline JS 177KB。直覺以為 CSS 重,實測 JS 才是 parse 成本最高的。量之前別猜。

defer vs async 的判斷準則:外部 script 之間有載入順序依賴 → defer;完全獨立、不在意順序 → async。Calc 的 5 個 Pro JS 是強鏈,順序不能亂,必須 defer。

改 index.html 屬性也要 bump SW:不是只有內容改變才需要 bump。index.html 任何變動都要換 CACHE_NAME,否則 cache-first 的 SW 不會替換舊版。

PSI 公開 API 有 quota 限制googleapis.com/pagespeedonline/v5/runPagespeed 無 API key 直接 429。自動化量 CWV 需要申請 API key,或改用 pagespeed.web.dev 網頁版手動量,或本地跑 npx lighthouse。

來源:個人開發日誌 2026-06-18 · ∑ Calc v3.8.2 · commit f6ba756 · index.html 5780 行 / inline JS 177KB / 外部 JS 5 個 × 14KB gzip · GitHub Pages + Fastly CDN