Core Web Vitals:用 LCP、CLS、INP 三個指標拆解前端效能
文字解析 · 1 支影片 · 產生於 2026-10-01 00:54
章節(13)
1. Outline
- 起點 · Core Web Vitals Explained: LCP, INP & CLS
從「效能到底該量什麼」出發,對 LCP、CLS、INP 三個指標各跑一次同樣的推導:動畫拆瀏覽器時序 → 指認被擋住的那一步 → 用最小的程式碼改動解開 → 在 DevTools 上讓數字驗證。
2. YouTuber 的思維推導
Core Web Vitals Explained: LCP, INP & CLS
I Code It · 14m46s · 字幕 en · vision=on (使用者指定 vision=true;影片本身大量依賴動畫、程式碼對照與 Chrome DevTools 數字,不看畫面讀不懂前後差異。) · YouTube
預估 vs 實際耗時
| 階段 | 預估 | 實際 |
|---|---|---|
| fetch | 6s | 2s |
| segment | 1m00s | 5m00s |
| shot | 41s | 6s |
| analyze | 6m15s | 11m40s |
| render | 5s | – |
作者從一個提問出發:想改善網站效能,第一件要決定的事永遠是「到底該量什麼」。他把 Core Web Vitals 當成這個提問的答案——不是一堆零散數字,而是三個對應使用者感受的面向:內容多快出現(LCP)、版面穩不穩(CLS)、操作有沒有反應(INP)。接著他對三個指標各跑同一套推導模板:先用動畫拆開瀏覽器內部的時序(誰在等誰),從時序裡指認出一個「被擋住的環節」——LCP 是解析被阻塞腳本卡住、CLS 是尺寸未知所以沒預留空間、INP 是主執行緒被一段長工作佔滿;再用一組 before / after 的真實程式碼把那個環節解開(defer + preload + fetchpriority、width/height、把工作切塊並交還控制權),並且每次都在 Chrome DevTools 上讓數字自己說話。
最後他把三個結論收斂成同一句話:改善 Web Vitals 就是「讓重要資源優先、讓版面可預測、不要讓主執行緒被長時間佔住」。整支影片的推理骨架因此是「先問量什麼 → 拆瀏覽器時序 → 找出被擋住的那一步 → 用最小改動解開它 → 用數字驗證」。
推理鏈:每段留給下一段的線索
每一列是一段,箭頭後的字是這段留給下一段的線索。點段落標題跳到該段。
- 先問:效能到底該量什麼 → 「三個指標」被端上桌但還沒拆開——載入、視覺穩定、互動回應各自對應哪一個名字、各自量的到底是哪一段時間,留給下一段先把地圖畫出來。
- 三個指標各自管什麼 → 三個指標的分工畫好了,但「最大的可見元素」是怎麼被瀏覽器認定的仍是黑箱——下一段要進到瀏覽器內部,看 LCP 這個時間點到底是怎麼被記下來的。
- LCP:最大元素畫完的那一刻 → 既然分數取決於「最大元素何時開始畫」,那只要有什麼東西讓瀏覽器晚一點才走到那個元素,分數就會壞掉——下一段就要指認出實務上最常見的那個兇手。
- 阻塞腳本為什麼拖慢 LCP → 推理說到這裡都還是理論,「LCP 會變差」到底差到什麼程度沒有數字——下一段把範例真的跑起來,讓 DevTools 給出這條因果鏈的量化證據。
- 實測 before:LCP 5.4 秒 → 5.41 秒的病歷擺在桌上了,但那支慢腳本是業務需要、不能直接刪掉——下一段要在保留它的前提下,看有哪些屬性可以把載入順序重新排過。
- 修法:defer、preload、fetchpriority → LCP 這條線收束在「讓重要內容早點出現」;但內容早出現不代表使用者讀得舒服——如果它出現時把別人擠開呢?這個新問題留給下一個指標。
- CLS:版面為什麼會亂跳 → 動畫裡「元素突然變大」的原因被歸給「尺寸事先不知道」,但程式碼裡的「尺寸不知道」到底長什麼樣、又是怎麼被寫出來的——下一段回到編輯器看現場。
- before:圖片沒寫寬高就會擠壓版面 → 病因確認在「瀏覽器不知道要留多少空間」,那最小的處方應該就是「先告訴它」——下一段驗證這個猜想到底夠不夠。
- after:寫上寬高,CLS 歸零 → 載入(LCP)與版面(CLS)都收束了,但兩者都發生在使用者「還在看」的階段;等使用者真的動手按下去,瓶頸會換成另一種——這留給最後一個指標。
- INP:主執行緒被長工作卡住 → 「使用者按了卻沒反應」目前還只是動畫上的說法,在開發機那種快 CPU 上根本重現不出來——下一段要想辦法把這個現象放大到看得見。
- 20 倍降速實測:卡住 vs 有反應 → after 頁面在同樣 20 倍慢的 CPU 上卻按得動,運算量並沒有變少——那它到底改了什麼,這個謎題留給下一段揭曉。
- 修法:切成小塊並交還控制權 → 三個指標各自的機制、病灶與處方都走完了,剩下的是把三條線收在一起——總結收束前的最後一步,看它們是不是同一個道理的三種說法。
- 總回顧:三個指標三句話
3. 逐段說明
Core Web Vitals Explained: LCP, INP & CLS
1. 先問:效能到底該量什麼 0:00–0:26
作者開場先丟出一個問題:想改善網站效能,第一件事永遠是「我們到底該量什麼」。Core Web Vitals 就是對這個問題的回答,用三個指標分別涵蓋載入、視覺穩定與互動回應。他預告這支影片會逐一走過三個指標,並帶你看瀏覽器內部實際發生了什麼。
推理因為前情提要裡最缺的是一把共同的量尺,所以作者不從優化技巧講起,而是先把問題往前推一步:任何優化都得先有可量測的定義,否則「變快了」只是感覺。他接著把 Core Web Vitals 定位成對這個問題的正式回答——用三個指標分別對應載入、視覺穩定與互動回應,並預告會帶你看瀏覽器內部實際發生什麼,而不只是背數字。
AI 補充作者沒說的背景是這套指標的來歷:Core Web Vitals 是 Google 在 2020 年提出、並納入搜尋排名訊號的一組使用者體驗指標,所以它不只是開發者自我要求,還會影響流量。它也不是固定不變的——最早的三個是 LCP、FID、CLS,2024 年 3 月起 INP 正式取代了 FID,原因正是 FID 只量「第一次互動的等待時間」,量不到互動之後畫面多久才更新。理解這個替換史,就能理解為什麼這支影片的第三個指標特別強調「到下一次繪製」。另外要先建立一個觀念:這三個指標刻意選的都是「使用者看得到、感覺得到」的事件,而不是 DOMContentLoaded、load 這類瀏覽器內部里程碑——後面每一段的推導都是從這個立場長出來的。
2. 三個指標各自管什麼 0:26–1:10
LCP(Largest Contentful Paint)量主要內容多快出現,CLS(Cumulative Layout Shift)量版面有多穩定,INP(Interaction to Next Paint)量頁面對使用者操作的反應有多快。三者對應使用者體驗的三個面向。作者也說明後面每個指標的講法固定:先用動畫解釋原理,再看 before / after 的程式碼範例。
推理因為上一段已經確立「要量使用者感受得到的事」,所以作者接著把使用者感受切成三塊來分配:眼睛看到主要內容(LCP)、閱讀時版面不亂跳(CLS)、手指按下去有反應(INP)。三者互不重疊,合起來覆蓋一次瀏覽從打開到操作的完整體驗。他同時先公布後面的講法模板——每個指標都先用動畫解釋原理,再看一組 before / after 的程式碼——等於預先告訴你每一章要拿什麼當證據。
- CLCP:主要內容多快出現在畫面上,量的是繪製完成的時間點→ 畫地圖
- CCLS:整個頁面生命週期累加的版面位移分數,好的分數低於 0.1→ 畫地圖
- CINP:使用者互動到畫面下一次更新的時間,理想低於 200 毫秒→ 畫地圖
AI 補充值得補上的是三個指標量測的「時間窗」並不一樣,這是後面所有推理的隱藏前提:LCP 是一個時間點,在載入期間隨候選元素更新,通常在使用者第一次互動或捲動後就凍結;CLS 是一段累加值,整個頁面生命週期都在累加,不是只算載入那幾秒;INP 則要等使用者真的操作才存在,沒有互動就沒有 INP(後面 DevTools 畫面上 INP 顯示成一橫槓就是這個原因)。另外作者用的「before / after 對照」不只是教學排版,它其實是效能工作的標準方法:固定其他變因、只改一處、看同一個數字怎麼動——後面三章你會看到同一個手法重複三次。
3. LCP:最大元素畫完的那一刻 1:10–2:21
LCP 量的是視窗內最大的可見元素完成繪製所需的時間,通常是 hero image、大標題或主要內容區塊。它問的不是「頁面何時開始載入」,而是「主要內容何時真的出現在使用者眼前」,Google 建議控制在 2.5 秒內。動畫顯示瀏覽器會邊載入邊更新「目前最大元素」的候選,直到最大的那個畫完才記下 LCP 時間。

推理因為上一段已經說 LCP 量的是「主要內容多快出現」,所以作者接著要處理一個必然冒出來的疑問:頁面上元素是陸續出現的,瀏覽器怎麼知道哪一個才算「最大」?他的回答是把 LCP 描述成一個持續更新的候選機制:先出現的小元素暫時當候選,出現更大的就換人,最後最大的那個完成繪製時記下時間。他也刻意把 LCP 和「頁面何時開始載入」對立起來,強調這個指標問的是更貼近使用者的問題。
AI 補充動畫(畫面上標題就寫「How LCP is calculated」,最大的那塊被標成 LCP candidate)沒說清楚三件事。第一,「最大」比的是元素在視窗內的可見面積,超出視窗的部分不算,圖片還會取「內在大小」與「顯示大小」中較小者,所以把一張巨圖縮小顯示並不會讓它變成更大的候選。第二,候選只會往上換、不會往下換,這也是為什麼作者說「最後最大的那個畫完就記下時間」。第三,這個更新過程不是無限持續的:使用者第一次點擊、按鍵或捲動之後,LCP 就停止更新——因為使用者已經開始操作,再晚出現的內容對「第一印象」已無意義。理解這三點,才會知道為什麼下一段的阻塞問題傷得那麼重:被延後的偏偏是那個決定分數的最大元素。
術語:viewport
4. 阻塞腳本為什麼拖慢 LCP 2:21–3:30
範例頁面在 header 放了一支很慢的 source.bundle.js,因為是阻塞式腳本,瀏覽器必須停下 HTML 解析等它下載並執行完。原因很單純:JavaScript 可以改 DOM,可以插入、刪除節點,瀏覽器不能在腳本跑完前安全地繼續建構頁面。結果是瀏覽器卡在 header,還沒讀到 hero image 那段 HTML,圖片請求要等腳本結束才發出,最重要的內容反而最晚開始載入。
推理因為上一段已經確立 LCP 取決於最大元素被畫出來的時刻,所以作者接著找出擋在這個時刻前面的東西:範例頁面在 header 放了一支很慢的 source.bundle.js(裡面刻意塞了一個重迴圈)。他沒有停在「腳本會擋住頁面」這種說法,而是往下追一層問「為什麼非得擋」,答案是 JavaScript 有權插入、刪除節點、整個改掉頁面結構,瀏覽器在腳本跑完前無法安全地繼續把頁面建起來。於是因果鏈完成:解析暫停 → hero image 那段 HTML 讀不到 → 最重要的內容最晚載入 → LCP 變差。
- CRender-blocking script:腳本可能改掉頁面結構,瀏覽器得先讓它跑完才能安全繼續解析→ 畫地圖
- CPreload scanner:parser 被腳本卡住時,另外掃一遍原始 HTML 提前發出資源請求→ 畫地圖
- R瀏覽器在 script 處暫停解析的歷史原因→ 存+回想
AI 補充作者跳過了一個關鍵的中間步驟,也因此把結論說得太滿。瀏覽器之所以必須在 <script> 處暫停解析,根本原因是舊時代的 document.write:腳本有可能在解析當下往文件流裡寫入內容,改變接下來要解析的位元組,所以 parser 不能先跑過去。但現代瀏覽器早就有 preload scanner(預載掃描器)作為補償——parser 被腳本卡住時,它會另外把後面的原始 HTML 掃過一遍,先把 <img src>、<link>、<script src> 這些資源的請求發出去。所以在這個 demo 裡(hero image 是寫死在 HTML 的 <img src>,畫面上第 16 行看得到),圖片請求其實不會等到腳本跑完才發出。真正讓 LCP 爛掉的是另外兩件事:一是腳本佔住主執行緒,圖片就算下載完了也沒人能把它畫出來(LCP 記的是「畫完」不是「下載完」);二是腳本與圖片搶頻寬。這個區別很重要,因為它決定了下一段的修法為什麼要同時動 defer(解開繪製)和 preload(解開下載順序)兩個地方——只做一個是不夠的。
術語:render-blocking script
「which means the image request only starts after the script finishes」
5. 實測 before:LCP 5.4 秒 3:30–4:11
作者打開 LCP before 範例,可以看到腳本先載入一陣子,hero image 才出現,Chrome DevTools 的 Performance 分頁顯示 LCP 高達 5.4 秒。DevTools 預設就會列出三個核心指標,這時的 LCP 明顯不及格。


推理因為上一段已經把兇手指認完畢,所以作者接著做效能工作最重要的一件事——量給你看。他先回到編輯器讓你確認案發現場的結構:阻塞 script 在 header,LCP 元素(那張圖)在下面;再切到瀏覽器實際載入,肉眼就能看到腳本先載一陣子、hero image 才冒出來;最後打開 Chrome DevTools 的 Performance 分頁,讓 Local metrics 直接吐出 LCP 5.41 秒並標成 poor。前一段的理論到這裡才變成證據。
- A效能除錯的 before/after 對照,就是科學實驗的控制變因法→ 批判類比
- EDevTools 實測:腳本擋住解析的 before 頁面,LCP 是 5.41 秒,標成 poor→ 存+演練
- R本機數字 vs Chrome UX Report 真實使用者資料→ 存+回想
AI 補充畫面上有兩個細節值得指出來。第一,DevTools 的 Local metrics 面板同時列出三個指標,此刻 CLS 是 0、INP 是一橫槓——INP 沒有數字不是因為頁面很快,而是還沒有人跟頁面互動過(呼應第 2 段說的「沒有互動就沒有 INP」)。第二,面板下方那行「compare to real Chrome UX Report」提醒了一件作者沒展開的事:這個 5.41 秒是你這台機器、這一次載入的數字,而 Google 排名採用的是真實使用者的分佈(75 百分位)。本機數字的正確用法是做 before / after 的相對比較,不能拿來宣稱線上合格。順帶一提,這個 before 頁面刻意在無痕視窗、無快取的狀態下跑,否則第二次載入圖片直接命中快取,數字就沒有對照價值了。
術語:Chrome DevTools Performance panel
6. 修法:defer、preload、fetchpriority 4:11–5:42
改善版把腳本加上 defer,告訴瀏覽器它不必立刻執行、也不會改 DOM,於是解析 HTML、建 DOM 與下載腳本可以並行,腳本等解析完才跑。同時 preload hero image 讓請求盡早發出,並給它高 fetch priority 標示這個資源特別重要。實測即使 JavaScript 一樣慢,圖片因為被預載而立刻出現,LCP 大幅改善。關鍵想法是:別擋住瀏覽器取得最重要的內容,先給主視覺元素優先權,不重要的往後延。


推理因為上一段確認了問題出在順序而不是體積,所以作者的三個改動剛好對應第 4 段那條因果鏈上的三個環節:加 defer 讓腳本不再擋住解析,解析、建 DOM 與下載腳本可以並行,腳本等 HTML 解析完才執行;對 hero image 做 preload,讓請求盡早發出;再給它高 fetchpriority,告訴瀏覽器這個資源特別重要。他最後回到瀏覽器實測——JavaScript 一樣慢,但圖片立刻出現,LCP 從 5.41 秒掉到 0.10 秒——並把整章收束成一句話:別擋住瀏覽器取得最重要的內容。
- Adefer 保證解析完才依序執行,async 是下載完立刻插隊、順序不保證→ 批判類比
- E加 defer+preload+fetchpriority 後,LCP 從 5.41 秒掉到 0.10 秒→ 存+演練
- P替阻塞 LCP 的腳本加上 defer,並把最大元素的圖片加 preload + fetchpriority→ 練習
AI 補充先修正作者一句話(見本段勘誤):defer 並不代表這支腳本不會改 DOM,它的意思是「延到 HTML 解析完成之後才執行」。正因為執行時 parser 已經收工,腳本再怎麼改 DOM 都不會打斷解析,所以才安全——安全的來源是時機,不是腳本乖。這也是 defer 與 async 的分水嶺:async 是「下載完就立刻插隊執行」,執行時機不可預測、彼此也不保證順序,適合獨立的分析腳本;defer 保證在解析後、DOMContentLoaded 前,且多支之間維持書寫順序,適合有相依性的應用程式碼。至於 preload 與 fetchpriority 的分工也常被混為一談:preload 決定「什麼時候開始下載」(把請求提前到解析到那一行之前),fetchpriority="high" 決定「和其他請求搶頻寬時排第幾」,兩者一個管時間、一個管順位,所以會一起用。畫面上的 after 版本把三者放在同一份檔案裡(第 9 行 preload、第 11 行 defer、第 17 行 fetchpriority),可以和上一段的 before 逐行對照。最後別漏掉:0.10 秒這種數字是本機無快取、快速網路下的結果,真實使用者不會這麼漂亮,重點是改善的幅度而不是絕對值。
「It won't change the DOM element.」
7. CLS:版面為什麼會亂跳 5:42–6:48
CLS 量的是頁面載入過程中,可見內容非預期移動的程度:你正在讀一段文字,上方的圖片或廣告載入,文字就被擠下去,這就是一次 layout shift。CLS 把所有位移加總成分數,好的分數應該低於 0.1。動畫示範一個網格版面,某個元素因為圖片載入而突然變大,周圍元素就被迫讓位,每一次讓位都累加進分數。

推理因為上一段的修法只保證內容早點出現、沒保證出現得安分,所以作者接著換一個面向:版面穩定。他先用一個人人有共鳴的場景定義問題——你正在讀一段文字,上方的圖片或廣告載入,文字被擠走——再說明 CLS 把所有這類位移加總成分數,好的分數低於 0.1。接著用網格動畫演示機制:某個元素因為圖片載入而突然變大,周圍元素被迫讓位,接著另一個元素從正方形變長方形又推一次,每一次讓位都累加進分數。
AI 補充動畫(畫面下方的字幕就寫「1. Square grows to 2×2 → others reflow」)把機制講對了,但「把所有位移加總」是簡化的說法,有兩個修正值得知道。第一,只有「非預期」的位移才算:使用者互動後 500 毫秒內發生的移動被視為預期而豁免,所以點按鈕展開手風琴、按下去載入更多內容都不會被罰——這也是為什麼「先讓使用者按了才長出來」是合法的設計策略。第二,2020 年 6 月起 CLS 已經不是整頁生命週期一路加總,而是改用 session window:把位移切成一段段(間隔超過 1 秒或整段超過 5 秒就換一段),最後取分數最高的那一段。這個修正是為了不讓長時間停留的頁面(例如無限捲動的動態牆)被無止境地累加成天文數字。至於分數怎麼算,每次位移的分數是「受影響區域佔視窗的比例」乘上「移動距離佔視窗的比例」,兩個因子都以視窗為分母——記住這點,下一段作者示範改變視窗大小分數就變的現象才不會顯得神秘。
8. before:圖片沒寫寬高就會擠壓版面 6:48–8:22
before 版的 img 沒有指定 width 與 height,甚至沒有 src,只有 data-source,靠 JavaScript 延遲插入。頁面初次執行時瀏覽器不知道圖片要佔多少空間,就不會預留位置,等圖片載入時才硬插進版面,下方內容整個被往下擠。作者也示範同一個頁面改變視窗大小後再重整,CLS 分數會不同,因為位移只算視窗內、且圖片相對視窗的比例變大了。


推理因為上一段已經指出位移的根源是尺寸未知,所以作者回到編輯器逐項確認這個條件是怎麼被製造出來的:img 沒有寫 width 與 height,甚至連 src 都沒有,只有 data-src,再靠一段 JavaScript 延遲把網址塞回去。條件湊齊之後結果是必然的——首次繪製時瀏覽器完全不知道這裡要留多少空間,於是留零;圖片一到就硬插進版面,下方內容整個被往下擠。他接著把同一個頁面拉成獨立視窗、改大小再重整,分數就不一樣,親自驗證了位移只算視窗內、且與圖片佔視窗的比例有關。
AI 補充這份 before 程式碼刻意疊了兩層「壞」,值得拆開看清楚,否則會誤以為只要有 data-src 就完蛋。第一層是 data-src + setTimeout 延遲插入(畫面上第 27–31 行分別排了 1500、2500、3500 毫秒),它繞過了第 4 段講的 preload scanner——掃描器只認原始 HTML 裡的網址,data-src 對它是隱形的,所以圖片必定在首次繪製之後才到。第二層才是沒寫 width/height,讓瀏覽器連預留空間都做不到。兩層缺一,現象都不會這麼戲劇化。另外請看 DevTools 的實測數字:CLS 是 0.04,其實仍在 0.1 的合格線內,旁邊還標著 good。這不是矛盾,而是提醒你 demo 的規模很小(只有三張圖、一個視窗高度);真實頁面上同樣的錯誤配上廣告位、動態橫幅與網頁字型換字(FOUT),輕易就會衝破門檻。作者示範的「改視窗大小分數就變」也正是這個道理的另一面:分數的分母是視窗,把視窗縮小,同一張圖佔的比例變大,同一次位移就被罰得更重。
術語:intrinsic size
9. after:寫上寬高,CLS 歸零 8:22–10:11
改善版只做一件事:幫 img 加上 width 與 height。瀏覽器就能算出正確的長寬比、立刻預留空間,實測 CLS 是 0。即使把網路節流到 fast 4G 拖慢圖片載入,CLS 依然是 0,因為版面在圖片載入完成前就已經知道要留多少位置。重點是:只要內容可能晚點才載入,就先讓瀏覽器知道它的尺寸。段末作者順帶介紹自己的 Front-End System Design Essentials 課程。

推理因為上一段把病因鎖定在資訊缺口而不是載入速度,所以作者的處方也就對準資訊本身:在 img 上補回 width 與 height,瀏覽器據此算出長寬比、在圖片還沒下載完之前就把空間預留好。他還做了一個很漂亮的反證——把網路節流到 fast 4G 讓圖片載得更慢,CLS 依然是 0。這證明版面穩定與載入快慢是兩件獨立的事:慢不會造成位移,資訊不足才會。整章的收束因此是:只要內容可能晚點才到,就先讓瀏覽器知道它的尺寸。
- AHTML 的 width/height 其實會被換算成 CSS 熟悉的 aspect-ratio→ 批判類比
- E把網路節流到更慢,補了 width/height 的頁面 CLS 依然是 0→ 存+演練
- P替所有內容圖片補回 width 與 height,讓瀏覽器提前預留空間→ 練習
AI 補充有一個容易被誤解的機制要補上:現代瀏覽器並不是把 HTML 屬性上的 width="3256" 當成死板的像素寬度,而是從 width 與 height 換算出 aspect-ratio 套進預設樣式表,所以你照樣可以用 CSS 寫 width:100%; height:auto 做響應式,空間一樣會被正確預留——這正是「寫死寬高會不會破壞 RWD」這個老疑慮的答案,2019 年之後已經不成立。對於長寬比真的無法預知的內容,替代方案是直接在容器上寫 CSS 的 aspect-ratio,或用固定高度的骨架佔位。另外要留意 after 這份檔案其實不只補了寬高(見本段勘誤):它同時把 data-src + setTimeout 那段延遲載入也拿掉、改回一般的 src。這不影響「預留空間能消除位移」的結論,但如果你照著改自己的專案、卻只補寬高而保留 lazy 插入,也一樣會得到 0——真正起作用的就是那兩個屬性。段末作者順帶介紹了自己的 Front-End System Design Essentials 課程,屬於業配段落,與推理無關。
「Here, we simply add width and height attribute to the image element.」
10. INP:主執行緒被長工作卡住 10:11–11:17
INP 量的是使用者操作到畫面下一次更新之間的時間,理想應低於 200 毫秒。問題情境是按鈕觸發一大堆運算:因為瀏覽器的 JavaScript 是單執行緒,這些重活會卡住主執行緒,瀏覽器既無法更新 UI 也無法回應其他互動。使用者按了按鈕卻什麼都沒發生,直到運算結束畫面才更新,INP 分數自然很差。

推理因為前面兩個指標都在處理「畫面呈現」,而使用者一旦開始操作就換成另一組期待,所以作者把 INP 定義成互動到畫面下一次更新之間的時間,理想低於 200 毫秒。接著他用同一套「拆時序找瓶頸」的手法:按鈕觸發一大堆運算,而瀏覽器裡的 JavaScript 是單執行緒,這些重活一旦跑起來就佔住主執行緒,瀏覽器既無法更新 UI 也無法回應其他互動。於是使用者按了按鈕卻什麼都沒發生,要等運算結束畫面才更新——這正是差勁 INP 分數的來源。
AI 補充作者的定義是對的,但少講了一層會影響你怎麼讀數字:INP 不是某一次互動的耗時,而是把整次造訪的所有互動延遲蒐集起來,取近乎最差的那一次來代表這個頁面(互動很多時會捨去少數極端值)。所以「有一次很卡」就足以拖垮分數,平均值再好也沒用——這也是為什麼下一段作者狂點按鈕,數字會那麼難看。另外,單次互動延遲其實由三段組成:input delay(事件排到隊之前的等待)、processing time(事件處理器本身執行)、presentation delay(處理完到瀏覽器實際畫出下一幀)。作者示範的「主執行緒被長工作佔住」主要放大的是第一段和第三段,而不是事件處理器本身寫得慢——這個區分很重要,因為它決定了下一段的修法為什麼是「把工作切開讓出時間」而不是「把運算寫得更快」。最後補充一個常被忽略的細節:把重運算包進 async/await 或 Promise 並不能解決問題,它們仍在同一條主執行緒上排隊。
11. 20 倍降速實測:卡住 vs 有反應 11:17–12:35
作者把 CPU 節流調成 20 倍慢來放大問題:before 頁面按下 process data 後,再怎麼點計數器都沒有反應,DevTools 的 INP 明顯偏長。after 頁面同樣是 20 倍慢的 CPU,處理進行中計數器仍然可以按、畫面持續更新,INP 分數也比較好。

推理因為上一段的因果鏈需要一個看得見的現場,而現代開發機太快、長工作一閃就過,所以作者先建立控制變因:CPU 20× slowdown、Fast 4G、停用快取。條件固定之後對照就成立了——before 頁面沒按處理鍵時計數器好好的,一按下 process data,再怎麼連點計數器都毫無反應,DevTools 的 INP 直接跳到 1,888 ms 並標成 poor;換到 after 頁面,同樣 20 倍慢的 CPU,處理進行中計數器仍然按得動、畫面持續更新,INP 明顯好轉。他甚至重整一次再示範一遍,確保你看到的不是偶發。
AI 補充畫面上藏著三個值得指出的細節。第一,頁面自己的說明文字寫著「watch the button:Processing… 從來不出現,你會直接從 Process Data 跳到 Done!」——這句話點出了 INP 的本質:不是運算慢,而是連「我收到了」這個回饋都畫不出來,因為要畫這一幀就得等主執行緒有空。第二,1,888 毫秒對照 200 毫秒的門檻是將近十倍,而這個數字正是「整次造訪最差那次互動」的體現:作者說他「其實點了很多下」,只要其中一下卡住就足以定調。第三,20 倍降速看起來誇張,但它模擬的是低階 Android 手機的真實處境;在開發機上跑不出問題卻在使用者端災難,正是效能問題最常見的盲區——所以節流不是作弊,而是把你的機器校正到使用者的水準。
12. 修法:切成小塊並交還控制權 12:35–13:40
改善的做法不是一次做完所有工作,而是把任務切成小塊:按下按鈕就先立刻更新 UI 顯示「處理中」,處理一小部分後把控制權交還瀏覽器,可以用 setTimeout 或分塊迴圈實作。因為週期性讓出控制權,瀏覽器有機會重繪畫面、回應其他互動,使用者立刻看到回饋。範例裡每塊只處理 500 筆,比一口氣跑一萬筆快得多。關鍵想法:盡可能讓主執行緒保持空閒,重活要切小。

推理因為上一段確認差別不在運算量,所以作者把答案指向執行的形狀而非大小:不再一次做完,而是切成小塊。按鈕一被按下就先立刻更新 UI 顯示「處理中」,接著處理一小部分,再把控制權交還瀏覽器,用 setTimeout 或分塊迴圈實作。因為週期性讓出控制權,瀏覽器就有空檔重繪畫面、回應其他互動,使用者立刻看到回饋。範例裡每塊只有 500 筆,遠比一口氣跑一萬筆友善。整章因此收束成:盡可能讓主執行緒保持空閒,重活要拆小。
- CTask chunking:把重運算切成小塊,塊與塊之間讓出主執行緒給瀏覽器繪製→ 畫地圖
- Etask chunking 程式碼:每塊只跑 500 筆,用 setTimeout(…, 0) 排到隊尾→ 存+演練
- P把一個會卡住畫面的重運算切成小塊,讓主執行緒有空檔繪製→ 練習
- R什麼情況該用 Web Worker 而不是切塊→ 存+回想
AI 補充畫面上的 inp-after.js 把這個手法寫得非常清楚,值得逐行讀。processNextChunk() 只跑 startIndex 到 startIndex + chunkSize 這一段(CHUNK_SIZE = 500),跑完如果還有剩,就用 setTimeout(…, 0) 把下一塊排進新的巨集任務——關鍵就在這個 0 毫秒:它不是「等 0 秒」,而是「排到隊尾」,讓瀏覽器在兩塊之間有機會插進一次繪製與事件處理。另外注意第 53–61 行的順序:先把 btn.innerText 設成 'Processing…',再用 setTimeout 包住第一塊工作(註解直接寫著 let the browser paint before starting heavy work)。如果少了這一層包裝,同步設定文字之後緊接著就開跑,瀏覽器根本沒機會把「Processing…」畫出來——這正是 before 版本從 Process Data 直接跳到 Done! 的原因。這裡也要提醒一個對照:如果這些運算完全不碰 DOM,更徹底的做法是丟給 Web Worker,主執行緒就完全不會被打擾;分塊適用於必須在主執行緒上、或需要中途更新畫面的情況。至於用什麼方式讓出控制權,setTimeout 是最通用的寫法,但已不是今天的首選(見本段勘誤)。
術語:task chunkingyielding to the main thread
「This can be done using the technique like set timeout or uh chunk loop.」
13. 總回顧:三個指標三句話 13:40–14:46
LCP 管載入效能,要讓最重要的內容盡早出現;CLS 管視覺穩定,要為圖片與動態內容預留空間;INP 管回應速度,主執行緒被長工作擋住頁面就無法及時回應。整體來說改善 Web Vitals 就是三件事:優先處理重要資源、讓版面可預測、避免主執行緒上的長時間阻塞工作。作者建議自己把 demo 專案跑起來比較 before 與 after。
推理因為三章各自的推導都已完成,所以作者不再重講機制,只留每個指標最後的那句操作原則:LCP 管載入效能,要讓最重要的內容盡早出現;CLS 管視覺穩定,要為圖片與動態內容預留空間;INP 管回應速度,主執行緒被長工作擋住頁面就無法及時回應。接著他把三句話再壓成一句——優先處理重要資源、讓版面可預測、避免主執行緒上的長時間阻塞工作——並建議你把 demo 專案跑起來自己比較 before 與 after,讓結論落回可驗證的地方。
AI 補充把整支影片倒過來看,三章其實共用同一個推理模板:先問這個指標量的是使用者的哪一種感受,再拆開瀏覽器在那段時間裡的工作順序,從順序裡指認出「誰在等誰」,最後用最小的改動把那個等待解開。三個病灶也可以歸成同一句話——瀏覽器需要的資訊或時間被別的東西佔走了:LCP 是繪製的時機被腳本佔走,CLS 是排版所需的尺寸資訊缺席,INP 是回應所需的執行緒時間被長工作佔走。最後補兩件作者沒說、但你實際去做時一定會撞到的事。其一,這支影片全程用的是本機 Local metrics,而 Google 判定合格看的是 CrUX 的真實使用者資料、取 28 天的 75 百分位,所以本機做出漂亮數字之後,仍要回頭看 Search Console 或 PageSpeed Insights 的 field data 才算數;本機數字的正確用途是相對比較。其二,這三個指標會演進——INP 在 2024 年 3 月才取代 FID——所以與其背門檻,不如記住這一套「拆時序、找被擋住的那一步」的方法,指標換了它依然管用。
4. 總結
作者先把問題從「網站慢」翻譯成「該量什麼」,用 Core Web Vitals 的三個指標分別對應載入、視覺穩定與互動回應。接著三次重複同一套推理:LCP 的時序顯示分數取決於最大元素何時開始畫,於是 header 裡的阻塞腳本成為兇手(因為 JS 能改 DOM,瀏覽器不敢繼續解析),DevTools 量到 5.4 秒,用 defer + preload + fetchpriority 把載入順序重排就大幅改善;CLS 的動畫顯示位移來自「尺寸事先未知所以沒預留空間」,程式碼現場是沒有 width/height 的圖片,補上兩個屬性後即使節流到 4G,分數仍是 0;INP 的問題是主執行緒被一段長工作佔滿,用 20 倍 CPU 降速把現象放大後,把運算切塊並週期性交還控制權,同樣的運算量就變得按得動。三條線收束成同一句話:讓重要資源優先、讓版面可預測、不要讓主執行緒被長時間佔住——都是在替瀏覽器安排「什麼時候該做什麼」。
勘誤總整理
確定錯誤/已過時見仁見智(取決於版本或情境)
| 段落 | 原話(transcript 逐字) | 說明 |
|---|---|---|
| 4. 阻塞腳本為什麼拖慢 LCP 3:17 |
「which means the image request only starts after the script finishes」 | 在這個範例裡並不成立。hero image 是直接寫在 HTML 裡的 <img src>,即使主解析器被 header 的阻塞腳本卡住,Chrome 的 preload scanner 仍會掃描後續 HTML 並提早發出圖片請求。LCP 之所以還是爛,真正原因是主執行緒被腳本佔住、圖片下載完也畫不出來(LCP 記的是完成繪製的時刻),再加上腳本與圖片搶頻寬。作者的說法只有在圖片由 JavaScript 動態插入、或網址藏在 data-src / CSS 背景圖裡時才正確。 依據: preload scanner 自 2008 年起即為主流瀏覽器標準行為;web.dev〈Don't fight the browser preload scanner〉(2022) |
| 6. 修法:defer、preload、fetchpriority 4:22 |
「It won't change the DOM element.」 | defer 並不保證腳本不會改動 DOM,也不需要這個保證。defer 的定義是「延到 HTML 解析完成之後才執行」,腳本執行時 DOM 已經完整,因此它可以(而且通常就是要)修改 DOM。之所以安全,是因為執行時機已經避開解析階段,而不是因為腳本不碰 DOM。真正會因為改動 DOM 而不安全的是解析期間執行 document.write 的同步腳本。 依據: HTML Living Standard:defer 腳本在解析完成後、DOMContentLoaded 之前依序執行 |
| 9. after:寫上寬高,CLS 歸零 8:24 |
「Here, we simply add width and height attribute to the image element.」 | after 版本改動的不只是寬高。對照畫面上兩份檔案:before 的 img 用 data-src 加一段 setTimeout 腳本延遲插入,after 則直接寫 src 並補上 width/height,等於同時拿掉了延遲載入。結論本身沒錯(預留空間確實是消除位移的關鍵),但這組 before/after 並非只差一個變因,嚴格說不算單變因對照;照著只補寬高、保留 lazy 插入的頁面,一樣可以把 CLS 降到 0。 依據: 影片 07:27 的 cls-before.html 與 08:37 的 cls-after.html 逐行對照 |
| 12. 修法:切成小塊並交還控制權 12:54 |
「This can be done using the technique like set timeout or uh chunk loop.」 | setTimeout(…, 0) 仍然可行且相容性最好,但已不是今天的首選寫法。它讓出控制權之後,任何其他排隊中的工作都可能插到你前面,導致整批處理被無限拖長;瀏覽器現已提供 scheduler.yield()(Chrome 129+,2024 年 9 月起)與 scheduler.postTask(),讓出之後仍保有優先權接續執行。另外要小心的是別把 Promise.resolve().then() 當成讓出——微任務會在同一輪清空,畫面照樣不會更新。 依據: scheduler.yield() 自 Chrome 129(2024-09)起正式提供;web.dev〈Optimize long tasks〉建議優先使用 scheduler.yield() |
5. 推薦三個下一步
1. 往下挖深:資源載入優先權的完整機制
影片用 preload 與 fetchpriority 解開了 LCP,但沒說明瀏覽器原本是依什麼規則排資源優先序、preload 與 prefetch、lazy loading 的分工在哪裡——不補這塊,換一個頁面就不知道該對哪個資源動手。
YouTube 搜尋:preload prefetch preconnect explained browser resource priority fetchpriority critical rendering path optimization
2. 往旁邊對照:主執行緒排程與 Web Worker
INP 那段只示範了 setTimeout 分塊,但同一個問題還有 scheduler.yield、requestIdleCallback 與 Web Worker 等做法,各自的取捨沒有被比較過。
YouTube 搜尋:scheduler.yield INP optimization web worker vs main thread javascript long tasks total blocking time
3. 往上應用:在真實產品裡持續監測這三個指標
影片的數字都來自本機 demo(lab data),但 Core Web Vitals 實際是以真實使用者的欄位資料評分;要在產品上用,得知道怎麼收集、怎麼看分佈與 75 百分位。
YouTube 搜尋:real user monitoring core web vitals CrUX field data vs lab data web-vitals js library tutorial
- 接著看 ←15 Frontend Concepts Every Senior Dev Has Mastered · 那站用渲染路徑掛出 LCP/INP/CLS 的位置,這站把三個指標逐一拆到瀏覽器時序
- 接著看 ←9 JavaScript Concepts That Got Me To Senior Dev · 先懂 event loop 與單執行緒,才看得懂 INP 為何被一段長工作卡住
- 接著看 →Frontend System Design: Performance API, Chrome, React Profiler · 三個指標的成因懂了,接著學怎麼用 Performance API 與 Profiler 量它們
- 相關 —Frontend System Design at Scale (The Senior Dev Playbook) · 同談 Core Web Vitals 與 defer:一個講機制,一個講大規模架構下的取捨
- 相關 —Real Frontend System Design (from a Senior Engineer) · 面試情境裡會用到這裡的 Core Web Vitals 與 Web Worker 結論
- 相關 —Top 15 Frontend Interview Questions for 2026 (wa/ Senior Engineer) · main thread 與 reflow 在面試題裡以問答形式再出現一次
- 接著看 ←The ultimate guide to web performance · 這站六分鐘建立三個指標的輪廓,那站把過時的 FID 換成 INP 並拆到瀏覽器時序