一、那個對不上的體感
這個場景你大概遇過,或者即將遇到:Day 21 的報告寫著全綠,/api/pizza p95 只有 180ms,門檻 800ms 綽綽有餘。然後產品經理走過來,把手機放在你桌上:「你自己開開看,轉圈轉三秒。」你開了,真的轉三秒。數據沒有錯,抱怨也沒有錯——你們量的不是同一件事。
前面二十五天做的都是協定層測試:k6 假裝自己是瀏覽器,直接對 API 發 HTTP 請求,量的是「伺服器多快把回應交出來」。但使用者看到的畫面,在回應交出來之後才開始:瀏覽器要下載 HTML,發現裡面還有 CSS、JavaScript、圖片、字型,一樣一樣抓回來;JavaScript 要執行,框架要啟動,再回頭呼叫那個 180ms 的 API;拿到資料還要把畫面畫出來——API 的 180ms,只是這條長路裡的一小段。

圖 1:同一次「打開網頁」,協定層量到的與使用者經歷的——180ms 之後還有一整條路
所以兩層測試回答不同的問題,誰也不能取代誰:
表的最後一列先記住,第五節會回來算這筆帳。先看瀏覽器層用什麼指標說話。
二、Core Web Vitals:使用者體感的標準答案
「使用者覺得慢」聽起來很主觀,但這件事早就被標準化了。Google 定義的 Core Web Vitals(核心網頁指標)就是把「體感」拆成三個可以量的數字,而且給了業界公認的及格線——你不用再和 PM 爭論「多慢算慢」,這題有標準答案,連 SEO 排名都看它。
LCP(Largest Contentful Paint,最大內容繪製):主要內容多久出現。從開始載入到「畫面上最大那塊東西」(主圖、標題區塊)出現的時間。它就是「轉圈轉三秒」的那三秒——使用者判斷「開好了沒」,看的就是這個。及格線:2.5 秒內算好,超過 4 秒算差。
CLS(Cumulative Layout Shift,累計版面位移):畫面有多會跳。載入過程中版面位移的累計量——你正要按「送出」,一張廣告圖片載入完成把按鈕擠下去,你按到別的東西,就是 CLS 在懲罰的行為。它量的不是快慢,是穩不穩。及格線:0.1 以下算好,超過 0.25 算差(它是一個位移比例的加總,沒有單位)。
INP(Interaction to Next Paint,互動到下次繪製):點下去多久有反應。使用者點按鈕、打字之後,畫面多快給出視覺回饋。它抓的是「點了沒反應,再點一次」的那種卡——通常是 JavaScript 忙到沒空理你。及格線:200ms 內算好,超過 500ms 算差。(舊資料會看到 FID,INP 已在 2024 年取代它;Claude Code 若寫出 FID,提醒它換掉。)

圖 2:三個 Core Web Vitals 與及格線——體感被拆成「多快出現、穩不穩、點了多快有反應」
注意這三個指標和 p95 的關係:Web Vitals 是每一次頁面載入量一次的指標,所以它們一樣有分佈、一樣看百分位——Google 官方看的是第 75 百分位。Day 3 的觀念原封不動搬過來:平均沒有意義,看 p75、p95。
三、k6 browser:同一個工具,走進瀏覽器
k6 內建了 browser 模組:它會啟動一個真的 Chrome(預設無頭模式,不開視窗),像真使用者一樣打開網頁,並自動記錄 Web Vitals。你不用學新工具,指標直接進 k6 的 summary,thresholds 一樣能當裁判——Day 7 的裁判制度直接適用於 LCP。
讓 Claude Code 生成,要求說清楚兩件協定層沒有的事:要一個 browser 的 scenario、要在頁面上做什麼動作:
Prompt 1|第一支 k6 browser 腳本
請幫我寫一支 k6 browser 腳本(tests/browser-home.js),需求:
- 目標:https://quickpizza.grafana.com(QuickPizza 的網頁,不是 API)
- 1 個瀏覽器 VU,跑 5 次 iteration,無頭模式
- 每次 iteration:開首頁 → 等主要內容出現 → 點「Pizza, Please!」按鈕 → 等推薦結果出現
- thresholds:LCP p75 < 2500ms、CLS p75 < 0.1
- 請用目前穩定版的 browser 模組寫法(import 路徑與 scenario 的
browser type 設定請以官方文件現行寫法為準),並逐段解釋
- 告訴我執行指令,以及如果我想「開著視窗看它操作」要加什麼環境變數
生成的骨架大致如下(import 路徑與細節以 Claude Code 查到的現行寫法為準):
import { browser } from 'k6/browser';
import { check } from 'k6';
export const options = {
scenarios: {
ui: {
executor: 'shared-iterations',
vus: 1,
iterations: 5,
options: { browser: { type: 'chromium' } }, // 用機器上的 Chrome
},
},
thresholds: {
browser_web_vital_lcp: ['p(75)<2500'],
browser_web_vital_cls: ['p(75)<0.1'],
},
};
export default async function () {
const page = await browser.newPage();
try {
await page.goto('https://quickpizza.grafana.com');
await page.locator('button[name="pizza-please"]').click();
await page.waitForSelector('h2'); // 等推薦結果出現
check(page, { '有拿到推薦結果': async p =>
(await p.locator('h2').textContent()) !== '' });
} finally {
await page.close();
}
}
幾個第一次見面的東西。async/await:瀏覽器操作都要等(等頁面開、等元素出現),所以到處是 await——不用深究,記得「瀏覽器腳本的動作前面都有 await」即可,Claude Code 會寫對。locator:用 CSS 選擇器找到頁面上的元素再操作,做過 UI 自動化的人會覺得眼熟——k6 browser 的頁面操作和 Playwright 是同一路的概念,但目的不同:Playwright 驗證「功能對不對」,k6 browser 量「體驗多快」。page.close():瀏覽器 VU 的資源很貴,不關會累積成災,所以放在 finally 裡保證執行。
跑起來之後,summary 會多出一排 browser_web_vital_* 指標——LCP、CLS、INP、FCP、TTFB 都在,看 p75 和 p95,裁判邏輯與 Day 7 完全相同。想親眼看它操作,加環境變數把無頭模式關掉(K6_BROWSER_HEADLESS=false),你會看到一個 Chrome 自己打開、自己點披薩——第一次看很療癒,之後記得關回來,有頭模式更吃資源。
四、判讀:TTFB 把鍋分給前後端
瀏覽器層的數據出來後,第一個要學的判讀是把「慢」分家:慢在伺服器,還是慢在前端?分家的關鍵指標是 TTFB(Time to First Byte,第一個位元組時間)——從發出請求到收到伺服器第一個位元組,它大致等於你在協定層量的東西。
簡單的算術:LCP = TTFB + 前端的部分(下載、解析、渲染)。所以:
• TTFB 高、LCP 也高 → 慢在伺服器或網路,回到協定層(Day 1–25 的整套方法)去查——瀏覽器層只是幫你確認了「後端真的慢」
• TTFB 低、LCP 高 → 伺服器很快交了貨,慢在前端:圖片太大、JavaScript 太肥、渲染被阻塞——這是協定層永遠看不到的那一段,也是「API 很快使用者卻覺得慢」謎題的標準解答
• LCP 好、INP 差 → 畫面出得快,但點了卡——JavaScript 在主執行緒上忙,互動排不上隊;這鍋幾乎純前端
把這個分家邏輯教給 Claude Code,讓它拿到數據先做這件事:
Prompt 2|Web Vitals 分家分析
這是 k6 browser 的 summary。請:
1. 列出 LCP、CLS、INP、TTFB 的 p75 與 p95,對照及格線標好/待改善/差
2. 用 LCP 與 TTFB 的關係判斷:慢的部分主要在伺服器還是前端?說明推理並標註〔推論〕
3. 如果是前端,列出最可能的三類原因與各自的下一步驗證方法
4. 產出兩句話:一句給後端 RD、一句給前端 RD,各自說明這份數據裡屬於他的部分
第 4 點是 Day 22 開單精神的瀏覽器版:同一份數據,前後端各有各的段落,單子才開得進對的待辦清單。
五、誠實的成本帳:為什麼不能「以後都這樣測」
現在回來算第一節那筆帳。一個協定層 VU 是一段輕量的程式,幾 MB 記憶體;一個瀏覽器 VU 是一整個 Chrome——數百 MB 記憶體、實打實的 CPU,資源消耗是協定層的百倍量級。你的筆電跑 500 個協定層 VU 沒問題,跑 10 個瀏覽器 VU 就開始煽風扇;而且瀏覽器 VU 一多,Day 19 的施壓端陷阱立刻回來——量到的是你的筆電渲染不動,不是網站慢。
所以瀏覽器層測試的用法從來不是「用它壓量」,而是:少量瀏覽器 VU 量體驗,體驗要在負載下量時,量由協定層去壓——這正是明天 Day 27 的混合場景。今天先把使用時機記成三條:

六、動手做:量出你的第一個 LCP
練習一:跑起來。用 Prompt 1 生成腳本執行。第一次跑先開視窗(K6_BROWSER_HEADLESS=false)看它操作一遍,確認動作正確,再關回無頭模式跑 5 次 iteration。讀 summary:LCP p75 過 2.5 秒了嗎?CLS 是多少?
練習二:分家。把 summary 交給 Prompt 2。QuickPizza 的 TTFB 從台灣量通常不低(伺服器在國外——Day 25 的地理位置這就派上用場了),看看 Claude Code 是把鍋分給網路距離還是前端,推理合不合理。
練習三:親手製造一個爛 LCP。把網路調慢:開著視窗跑,同時用作業系統或請 Claude Code 教你用 Chrome 的節流方式模擬 3G。看 LCP 從一點多秒變成七八秒——你量到的第一個「差」等級 Web Vitals,會讓你記住及格線是拿來跟誰比的:不是跟你的光纖,是跟使用者的捷運車廂。
七、注意事項:瀏覽器層測試的六個習慣

給 RD 的一句話:後端 RD 看到 TTFB 低而 LCP 高時,那份慢與你無關——是前端資產與渲染的問題;反過來 TTFB 高,QA 會帶著協定層數據回來找你。前端 RD 則值得把 LCP/CLS/INP 當成自己的 thresholds:QA 現在能在改版前後用同條件量出這三個數字,你的優化第一次有了不靠體感的成績單。
八、觀念驗證:三個問題確認你有帶走今天的重點
• 「API p95 180ms 但使用者轉圈三秒」為什麼兩邊都沒有錯?180ms 對應圖 1 的哪一段?(第一節)
• LCP、CLS、INP 各自量什麼?及格線是多少?為什麼說 TTFB 是「分家」的關鍵?(第二、四節)
• 瀏覽器 VU 的成本是協定層的什麼量級?這個事實如何決定了它的三種使用時機?(第五節)
九、小結
「API 很快,使用者還是覺得慢」不是誰量錯了,是兩層測試量的本來就不同:協定層量伺服器交貨的速度,瀏覽器層量畫面出現與可互動的速度,中間隔著下載、解析、渲染與 JavaScript。Core Web Vitals 把體感標準化成三個數字——LCP 多快出現、CLS 穩不穩、INP 點了多快有反應——而 TTFB 負責把鍋分給前後端。k6 browser 讓你用同一個工具走進瀏覽器,thresholds 照當裁判;但一個瀏覽器 VU 是一整個 Chrome,百倍量級的成本決定了它的定位:知道有這工具、知道何時需要——量體驗用它,壓量還是協定層。那如果想要「負載之下的體驗」呢?兩層一起上:協定層製造背景流量,少量瀏覽器 VU 在人潮中量真實體感——Day 27 見。