Core Web Vitals:用 LCP、CLS、INP 三個指標拆解前端效能

文字解析 · 1 支影片 · 產生於 2026-10-01 00:54

🎧 語音解析📝 筆記✍️ 練習

🎧 語音解析 18m33s · TTS 講解與作者原聲交錯;點章節可跳。
邊聽邊看逐句講稿,點任一句從那裡開始
章節(13)

1. Outline

  1. 起點 · 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 就是「讓重要資源優先、讓版面可預測、不要讓主執行緒被長時間佔住」。整支影片的推理骨架因此是「先問量什麼 → 拆瀏覽器時序 → 找出被擋住的那一步 → 用最小改動解開它 → 用數字驗證」。

推理鏈:每段留給下一段的線索

每一列是一段,箭頭後的字是這段留給下一段的線索。點段落標題跳到該段。

  1. 先問:效能到底該量什麼 → 「三個指標」被端上桌但還沒拆開——載入、視覺穩定、互動回應各自對應哪一個名字、各自量的到底是哪一段時間,留給下一段先把地圖畫出來。
  2. 三個指標各自管什麼 → 三個指標的分工畫好了,但「最大的可見元素」是怎麼被瀏覽器認定的仍是黑箱——下一段要進到瀏覽器內部,看 LCP 這個時間點到底是怎麼被記下來的。
  3. LCP:最大元素畫完的那一刻 → 既然分數取決於「最大元素何時開始畫」,那只要有什麼東西讓瀏覽器晚一點才走到那個元素,分數就會壞掉——下一段就要指認出實務上最常見的那個兇手。
  4. 阻塞腳本為什麼拖慢 LCP → 推理說到這裡都還是理論,「LCP 會變差」到底差到什麼程度沒有數字——下一段把範例真的跑起來,讓 DevTools 給出這條因果鏈的量化證據。
  5. 實測 before:LCP 5.4 秒 → 5.41 秒的病歷擺在桌上了,但那支慢腳本是業務需要、不能直接刪掉——下一段要在保留它的前提下,看有哪些屬性可以把載入順序重新排過。
  6. 修法:defer、preload、fetchpriority → LCP 這條線收束在「讓重要內容早點出現」;但內容早出現不代表使用者讀得舒服——如果它出現時把別人擠開呢?這個新問題留給下一個指標。
  7. CLS:版面為什麼會亂跳 → 動畫裡「元素突然變大」的原因被歸給「尺寸事先不知道」,但程式碼裡的「尺寸不知道」到底長什麼樣、又是怎麼被寫出來的——下一段回到編輯器看現場。
  8. before:圖片沒寫寬高就會擠壓版面 → 病因確認在「瀏覽器不知道要留多少空間」,那最小的處方應該就是「先告訴它」——下一段驗證這個猜想到底夠不夠。
  9. after:寫上寬高,CLS 歸零 → 載入(LCP)與版面(CLS)都收束了,但兩者都發生在使用者「還在看」的階段;等使用者真的動手按下去,瓶頸會換成另一種——這留給最後一個指標。
  10. INP:主執行緒被長工作卡住 → 「使用者按了卻沒反應」目前還只是動畫上的說法,在開發機那種快 CPU 上根本重現不出來——下一段要想辦法把這個現象放大到看得見。
  11. 20 倍降速實測:卡住 vs 有反應 → after 頁面在同樣 20 倍慢的 CPU 上卻按得動,運算量並沒有變少——那它到底改了什麼,這個謎題留給下一段揭曉。
  12. 修法:切成小塊並交還控制權 → 三個指標各自的機制、病灶與處方都走完了,剩下的是把三條線收在一起——總結收束前的最後一步,看它們是不是同一個道理的三種說法。
  13. 總回顧:三個指標三句話

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 這類瀏覽器內部里程碑——後面每一段的推導都是從這個立場長出來的。

Core Web Vitals

核心網頁指標

Google 定義的一組以使用者實際感受為準的網頁體驗指標,目前由 LCP、CLS、INP 三項組成。

2020 年由 Google 提出並在 2021 年納入搜尋排名訊號,用意是把「網站快不快」從工程師的主觀感覺變成三個可比較的數字。它挑的都是使用者感官上察覺得到的事件——內容出現、版面跳動、操作有沒有反應——而不是 load、DOMContentLoaded 這種瀏覽器內部里程碑。組成會隨時間調整:2024 年 3 月 INP 取代了原本的 FID。常見誤解是把它當成「跑分」,但它真正的判準是真實使用者的分佈(通常看 75 百分位),本機跑出來的漂亮數字不等於線上合格。

相關術語: Largest Contentful Paint (組成之一)、Cumulative Layout Shift (組成之一)、Interaction to Next Paint (組成之一)

出處:第 1 段「先問:效能到底該量什麼」

留給下一段 「三個指標」被端上桌但還沒拆開——載入、視覺穩定、互動回應各自對應哪一個名字、各自量的到底是哪一段時間,留給下一段先把地圖畫出來。

2. 三個指標各自管什麼 0:26–1:10

LCP(Largest Contentful Paint)量主要內容多快出現,CLS(Cumulative Layout Shift)量版面有多穩定,INP(Interaction to Next Paint)量頁面對使用者操作的反應有多快。三者對應使用者體驗的三個面向。作者也說明後面每個指標的講法固定:先用動畫解釋原理,再看 before / after 的程式碼範例。

承上 承上:上一段把「該量什麼」交給了 Core Web Vitals,但三個指標只有名目沒有內容;這一段就把三個名字和它們各自量的東西一一對上。

推理因為上一段已經確立「要量使用者感受得到的事」,所以作者接著把使用者感受切成三塊來分配:眼睛看到主要內容(LCP)、閱讀時版面不亂跳(CLS)、手指按下去有反應(INP)。三者互不重疊,合起來覆蓋一次瀏覽從打開到操作的完整體驗。他同時先公布後面的講法模板——每個指標都先用動畫解釋原理,再看一組 before / after 的程式碼——等於預先告訴你每一章要拿什麼當證據。

AI 補充值得補上的是三個指標量測的「時間窗」並不一樣,這是後面所有推理的隱藏前提:LCP 是一個時間點,在載入期間隨候選元素更新,通常在使用者第一次互動或捲動後就凍結;CLS 是一段累加值,整個頁面生命週期都在累加,不是只算載入那幾秒;INP 則要等使用者真的操作才存在,沒有互動就沒有 INP(後面 DevTools 畫面上 INP 顯示成一橫槓就是這個原因)。另外作者用的「before / after 對照」不只是教學排版,它其實是效能工作的標準方法:固定其他變因、只改一處、看同一個數字怎麼動——後面三章你會看到同一個手法重複三次。

Largest Contentful Paint

最大內容繪製(LCP)

量視窗內最大的可見元素完成繪製所花的時間,代表主要內容多快出現。

縮寫 LCP。它只看視窗(viewport)內的元素,且候選限定在圖片、影片封面、帶背景圖的區塊與區塊級文字這幾類。候選會隨載入過程被更大的元素取代,直到使用者第一次互動才停止更新。Google 的建議門檻是 2.5 秒以內(75 百分位)。常見誤解是把它當成「頁面載入完成時間」——它其實只在乎最大那一塊內容何時畫出來,頁面其他部分還在載也無所謂。

相關術語: Core Web Vitals (屬於)、Cumulative Layout Shift (並列指標)

出處:第 2 段「三個指標各自管什麼」

Cumulative Layout Shift

累積版面位移(CLS)

把頁面上所有非預期的可見內容位移加總成一個分數,代表版面有多穩定。

縮寫 CLS。它是三個指標裡唯一的「累加值」而不是時間,沒有單位,好的標準是低於 0.1。只計算「非預期」的位移:使用者互動後 500 毫秒內發生的移動會被視為預期而不計入,所以點按鈕展開選單不會被罰。分數與位移元素佔視窗的比例、以及移動的距離都有關,因此同一個頁面在不同視窗大小下分數會不同。

相關術語: Largest Contentful Paint (並列指標)、layout shift (由此累加)

出處:第 2 段「三個指標各自管什麼」

Interaction to Next Paint

互動到下次繪製(INP)

量從使用者操作到畫面下一次視覺更新之間的時間,代表頁面對操作的反應有多靈敏。

縮寫 INP,2024 年 3 月起取代了舊指標 FID 成為 Core Web Vitals 之一。FID 只量第一次互動「開始被處理」前的等待,量不到處理本身有多久、畫面何時真的更新;INP 涵蓋整段延遲,而且會看整次造訪中最慢的那幾次互動,不是只看第一次。建議門檻是 200 毫秒以內。沒有互動的頁面就沒有 INP 值。

相關術語: Core Web Vitals (屬於)、Largest Contentful Paint (並列指標)

出處:第 2 段「三個指標各自管什麼」

留給下一段 三個指標的分工畫好了,但「最大的可見元素」是怎麼被瀏覽器認定的仍是黑箱——下一段要進到瀏覽器內部,看 LCP 這個時間點到底是怎麼被記下來的。

3. LCP:最大元素畫完的那一刻 1:10–2:21

LCP 量的是視窗內最大的可見元素完成繪製所需的時間,通常是 hero image、大標題或主要內容區塊。它問的不是「頁面何時開始載入」,而是「主要內容何時真的出現在使用者眼前」,Google 建議控制在 2.5 秒內。動畫顯示瀏覽器會邊載入邊更新「目前最大元素」的候選,直到最大的那個畫完才記下 LCP 時間。

動畫的最後一格:最大元素畫完、瀏覽器記下 LCP 的瞬間,是理解「候選會被不斷取代」的關鍵畫面
2:09 · 動畫的最後一格:最大元素畫完、瀏覽器記下 LCP 的瞬間,是理解「候選會被不斷取代」的關鍵畫面
承上 承上:上一段留下「最大的可見元素怎麼被認定」這個黑箱,這一段用動畫把它打開——瀏覽器是一邊載入一邊更新候選,直到最大的那個畫完才定案。

推理因為上一段已經說 LCP 量的是「主要內容多快出現」,所以作者接著要處理一個必然冒出來的疑問:頁面上元素是陸續出現的,瀏覽器怎麼知道哪一個才算「最大」?他的回答是把 LCP 描述成一個持續更新的候選機制:先出現的小元素暫時當候選,出現更大的就換人,最後最大的那個完成繪製時記下時間。他也刻意把 LCP 和「頁面何時開始載入」對立起來,強調這個指標問的是更貼近使用者的問題。

AI 補充動畫(畫面上標題就寫「How LCP is calculated」,最大的那塊被標成 LCP candidate)沒說清楚三件事。第一,「最大」比的是元素在視窗內的可見面積,超出視窗的部分不算,圖片還會取「內在大小」與「顯示大小」中較小者,所以把一張巨圖縮小顯示並不會讓它變成更大的候選。第二,候選只會往上換、不會往下換,這也是為什麼作者說「最後最大的那個畫完就記下時間」。第三,這個更新過程不是無限持續的:使用者第一次點擊、按鍵或捲動之後,LCP 就停止更新——因為使用者已經開始操作,再晚出現的內容對「第一印象」已無意義。理解這三點,才會知道為什麼下一段的阻塞問題傷得那麼重:被延後的偏偏是那個決定分數的最大元素。

術語:viewport

viewport

視窗(可視區域)

瀏覽器目前實際顯示頁面的那塊矩形區域,捲動之前使用者看得到的範圍。

LCP 與 CLS 都以視窗為界:視窗外的元素不列入 LCP 候選,視窗外發生的位移也不計入 CLS。因為它是「相對」的框,同一個頁面在手機與桌機、或把瀏覽器視窗拉大拉小之後,指標數字都可能不同——後面 CLS 那一章作者親自示範了這件事。不要把它跟裝置螢幕尺寸混為一談,開了 DevTools 之後可視區域就變小了。

相關術語: Largest Contentful Paint (量測範圍)

出處:第 3 段「LCP:最大元素畫完的那一刻」

hero image

主視覺圖

頁面最上方、面積最大、用來建立第一印象的那張主要圖片。

在多數行銷頁與電商首頁上,hero image 幾乎必然就是 LCP 元素,所以它的載入路徑值得被特別對待——後面 preload 與 fetchpriority 針對的正是它。也因為它通常很大,把它換成現代格式(WebP/AVIF)、給對尺寸、避免用 CSS 背景圖(背景圖要等 CSS 解析完才會被發現)都直接反映在 LCP 上。

相關術語: Largest Contentful Paint (常見的量測對象)、preload (常被預載)

出處:第 3 段「LCP:最大元素畫完的那一刻」

LCP candidate

LCP 候選元素

載入過程中瀏覽器暫時認定為「目前最大」的那個元素,會被更大的元素取代。

瀏覽器每畫出一個新元素就比一次面積,比目前候選大就換掉,因此候選只會愈換愈大。最終被記錄下來的候選就決定了 LCP 的時間點。候選型別有限制:只有 <img>、<video> 的封面、帶 url() 背景圖的元素,以及區塊級的文字節點會被納入,純 CSS 畫出來的裝飾方塊不算。

相關術語: Largest Contentful Paint (決定其時間點)、viewport (限於其內)

出處:第 3 段「LCP:最大元素畫完的那一刻」

留給下一段 既然分數取決於「最大元素何時開始畫」,那只要有什麼東西讓瀏覽器晚一點才走到那個元素,分數就會壞掉——下一段就要指認出實務上最常見的那個兇手。

4. 阻塞腳本為什麼拖慢 LCP 2:21–3:30

範例頁面在 header 放了一支很慢的 source.bundle.js,因為是阻塞式腳本,瀏覽器必須停下 HTML 解析等它下載並執行完。原因很單純:JavaScript 可以改 DOM,可以插入、刪除節點,瀏覽器不能在腳本跑完前安全地繼續建構頁面。結果是瀏覽器卡在 header,還沒讀到 hero image 那段 HTML,圖片請求要等腳本結束才發出,最重要的內容反而最晚開始載入。

承上 承上:上一段留下「什麼會讓最大元素晚一點才開始畫」的問題,這一段給出最常見的答案——放在 header 裡的阻塞式 JavaScript。

推理因為上一段已經確立 LCP 取決於最大元素被畫出來的時刻,所以作者接著找出擋在這個時刻前面的東西:範例頁面在 header 放了一支很慢的 source.bundle.js(裡面刻意塞了一個重迴圈)。他沒有停在「腳本會擋住頁面」這種說法,而是往下追一層問「為什麼非得擋」,答案是 JavaScript 有權插入、刪除節點、整個改掉頁面結構,瀏覽器在腳本跑完前無法安全地繼續把頁面建起來。於是因果鏈完成:解析暫停 → hero image 那段 HTML 讀不到 → 最重要的內容最晚載入 → LCP 變差。

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

render-blocking script

阻塞繪製的腳本

沒有標記 defer 或 async 的 <script>,瀏覽器必須停下 HTML 解析、下載並執行完它才能繼續。

放在 <head> 的同步腳本殺傷力最大,因為它擋在整份 body 前面。歷史原因是 document.write 可以在解析當下改寫文件流,parser 因此不能先跳過去。同步的 <link rel="stylesheet"> 也會阻塞繪製(雖然不阻塞解析),常被一起討論。判斷方式很簡單:在 DevTools 的 Network 面板看這支腳本的下載與執行是否落在第一次繪製之前。

相關術語: defer (解法)、DOM (因可改動它)

出處:第 4 段「阻塞腳本為什麼拖慢 LCP」

DOM

文件物件模型

瀏覽器把 HTML 解析後建立起來的樹狀結構,也是 JavaScript 可以讀寫頁面的介面。

全名 Document Object Model。它是「解析 HTML」的產物,也是「畫出畫面」的輸入——瀏覽器要把 DOM 與 CSSOM 合成 render tree 才能排版與繪製。因為腳本能任意插入或刪除 DOM 節點,瀏覽器才不敢在腳本執行完之前繼續建構頁面,這正是這一段因果鏈的起點。注意 DOM 不等於你寫的那份 HTML 原始碼:JavaScript 改過之後兩者就分家了,DevTools 的 Elements 面板顯示的是 DOM。

相關術語: render-blocking script (被其改動)

出處:第 4 段「阻塞腳本為什麼拖慢 LCP」

preload scanner

預載掃描器

瀏覽器在主解析器被腳本卡住時,另外掃描後續 HTML 並提早發出資源請求的機制。

它是 parser 之外的第二個輕量掃描器,只認原始 HTML 裡直接寫出來的 URL。因此它救得了 <img src>、<link>、<script src>,卻救不了由 JavaScript 動態插入的圖片、CSS background-image、或只寫在 data-src 裡的網址——這也是後面 CLS 範例刻意用 data-src 的原因。知道它的存在,就能解釋一個常見疑問:為什麼把腳本改成 defer 之後圖片還是需要額外 preload。

相關術語: render-blocking script (補償其副作用)、preload (手動版)

出處:第 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)
留給下一段 推理說到這裡都還是理論,「LCP 會變差」到底差到什麼程度沒有數字——下一段把範例真的跑起來,讓 DevTools 給出這條因果鏈的量化證據。

5. 實測 before:LCP 5.4 秒 3:30–4:11

作者打開 LCP before 範例,可以看到腳本先載入一陣子,hero image 才出現,Chrome DevTools 的 Performance 分頁顯示 LCP 高達 5.4 秒。DevTools 預設就會列出三個核心指標,這時的 LCP 明顯不及格。

before 版 HTML:阻塞 script 在 header、LCP 元素(圖片)在下面,一眼看出兩者的先後關係
3:33 · before 版 HTML:阻塞 script 在 header、LCP 元素(圖片)在下面,一眼看出兩者的先後關係
Chrome DevTools Performance 分頁的三個核心指標與 5.4 秒的 LCP 數字,是 before 的量化證據
4:06 · Chrome DevTools Performance 分頁的三個核心指標與 5.4 秒的 LCP 數字,是 before 的量化證據
承上 承上:上一段的因果鏈只到「LCP 會變差」,這一段把 before 範例跑起來,用 DevTools 的數字把「差」講成一個具體的量。

推理因為上一段已經把兇手指認完畢,所以作者接著做效能工作最重要的一件事——量給你看。他先回到編輯器讓你確認案發現場的結構:阻塞 script 在 header,LCP 元素(那張圖)在下面;再切到瀏覽器實際載入,肉眼就能看到腳本先載一陣子、hero image 才冒出來;最後打開 Chrome DevTools 的 Performance 分頁,讓 Local metrics 直接吐出 LCP 5.41 秒並標成 poor。前一段的理論到這裡才變成證據。

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

Chrome DevTools Performance panel

Chrome 開發者工具效能面板

Chrome 內建的效能量測面板,會即時列出這次載入的 LCP、CLS、INP 三個本機數值。

面板上的 Local metrics 是「你這台機器、這一次載入」的量測,會受你的 CPU、網路、有沒有裝擴充功能影響,所以每次跑都會有些差異。它還會標出 LCP element 是哪一個節點(畫面上顯示的是 img),這在找兇手時比數字本身更有用。要做公平比較,習慣上會用無痕視窗、關掉快取、必要時開啟 CPU/網路節流——作者後面兩章都會用到這些開關。

相關術語: Chrome UX Report (對照的真實資料)、CPU throttling (其內建開關)

出處:第 5 段「實測 before:LCP 5.4 秒」

Chrome UX Report

Chrome 使用者體驗報告(CrUX)

Google 蒐集真實 Chrome 使用者造訪各網站時的 Web Vitals 資料集,是排名判定實際採用的來源。

常縮寫成 CrUX,屬於所謂的 field data(真實使用者資料),相對於你在本機跑出來的 lab data(實驗室資料)。判定是否合格看的是過去 28 天真實造訪的 75 百分位,也就是「四分之三的使用者體驗要達標」,而不是你開發機上那個漂亮數字。這解釋了一個常見困惑:Lighthouse 給 100 分,Search Console 卻說 LCP 不合格——兩者資料來源根本不同。

相關術語: Chrome DevTools Performance panel (相對的實驗室資料)、Core Web Vitals (其資料來源)

出處:第 5 段「實測 before:LCP 5.4 秒」

留給下一段 5.41 秒的病歷擺在桌上了,但那支慢腳本是業務需要、不能直接刪掉——下一段要在保留它的前提下,看有哪些屬性可以把載入順序重新排過。

6. 修法:defer、preload、fetchpriority 4:11–5:42

改善版把腳本加上 defer,告訴瀏覽器它不必立刻執行、也不會改 DOM,於是解析 HTML、建 DOM 與下載腳本可以並行,腳本等解析完才跑。同時 preload hero image 讓請求盡早發出,並給它高 fetch priority 標示這個資源特別重要。實測即使 JavaScript 一樣慢,圖片因為被預載而立刻出現,LCP 大幅改善。關鍵想法是:別擋住瀏覽器取得最重要的內容,先給主視覺元素優先權,不重要的往後延。

after 版 HTML:defer、preload、fetchpriority 三個改動同時出現在同一畫面,是與 before 對照的核心
4:47 · after 版 HTML:defer、preload、fetchpriority 三個改動同時出現在同一畫面,是與 before 對照的核心
after 的 DevTools 數字,和第 5 段的 5.4 秒直接對照才看得出改善幅度
5:20 · after 的 DevTools 數字,和第 5 段的 5.4 秒直接對照才看得出改善幅度
承上 承上:上一段留下的約束是「腳本一樣慢、不能刪」,這一段就在這個前提下動手,只改載入順序而不改腳本本身。

推理因為上一段確認了問題出在順序而不是體積,所以作者的三個改動剛好對應第 4 段那條因果鏈上的三個環節:加 defer 讓腳本不再擋住解析,解析、建 DOM 與下載腳本可以並行,腳本等 HTML 解析完才執行;對 hero image 做 preload,讓請求盡早發出;再給它高 fetchpriority,告訴瀏覽器這個資源特別重要。他最後回到瀏覽器實測——JavaScript 一樣慢,但圖片立刻出現,LCP 從 5.41 秒掉到 0.10 秒——並把整章收束成一句話:別擋住瀏覽器取得最重要的內容。

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 秒這種數字是本機無快取、快速網路下的結果,真實使用者不會這麼漂亮,重點是改善的幅度而不是絕對值。

defer

延後執行屬性

<script> 的屬性,讓腳本與 HTML 解析並行下載,並延到解析完成後才依書寫順序執行。

它同時解決了兩件事:下載不再阻塞解析,執行也移到 parser 收工之後。多支 defer 腳本會照它們在 HTML 裡出現的順序執行,且都在 DOMContentLoaded 事件之前跑完,所以有相依關係的應用程式碼用它最安全。常見誤解是以為 defer 代表「這支腳本不碰 DOM」——完全相反,它跑的時候 DOM 已經完整,正是操作 DOM 最好的時機。只對有 src 的外部腳本有效,內嵌 <script> 寫了也沒用。

相關術語: async (對照組)、render-blocking script (解除其阻塞)

出處:第 6 段「修法:defer、preload、fetchpriority」

async

非同步載入屬性

<script> 的屬性,腳本與解析並行下載,但一下載完就立刻執行、可能打斷解析。

和 defer 的差別全在執行時機:async 是「誰先下載完誰先跑」,順序不可預測,而且執行當下 DOM 可能還不完整。因此它適合完全獨立、不依賴頁面結構也不被別人依賴的腳本,例如分析或錯誤蒐集。若一組腳本彼此有相依性卻都掛 async,很容易出現時好時壞的競態錯誤。這支影片的範例選 defer 而不是 async,正是因為 bundle.js 是頁面自己的程式碼。

相關術語: defer (對照組)

出處:第 6 段「修法:defer、preload、fetchpriority」

preload

資源預載

用 <link rel="preload"> 告訴瀏覽器提早開始下載某個稍後一定會用到的資源。

它把請求的發起時間提前到解析走到那一行之前,特別適合那些瀏覽器「發現得太晚」的資源:CSS 裡的背景圖與字型、由 JavaScript 動態插入的圖片。要注意它只負責提早抓、不負責使用,抓了卻沒用到會被主控台警告,也等於白白浪費頻寬。用得太浮濫還會反過來排擠真正重要的資源——預載的東西愈多,等於誰都沒有優先權。

相關術語: preload scanner (手動指定版)、fetchpriority (常一起使用)

出處:第 6 段「修法:defer、preload、fetchpriority」

fetchpriority

抓取優先權

HTML 屬性,用 high 或 low 告訴瀏覽器某個資源在搶頻寬時該排在前面還是後面。

它管的是「順位」而不是「時間」,所以和 preload 是互補的:preload 讓請求早點發出,fetchpriority="high" 讓這個請求在連線資源有限時先被服務。對 LCP 元素標 high 是官方建議的做法之一;反過來,把折線以下的裝飾圖標 low 也同樣有效,因為騰出來的頻寬會回流給重要資源。它是相對的提示而非命令,瀏覽器仍可依自身啟發式調整。

相關術語: preload (常一起使用)、hero image (常套用於)

出處:第 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 之前依序執行
留給下一段 LCP 這條線收束在「讓重要內容早點出現」;但內容早出現不代表使用者讀得舒服——如果它出現時把別人擠開呢?這個新問題留給下一個指標。

7. CLS:版面為什麼會亂跳 5:42–6:48

CLS 量的是頁面載入過程中,可見內容非預期移動的程度:你正在讀一段文字,上方的圖片或廣告載入,文字就被擠下去,這就是一次 layout shift。CLS 把所有位移加總成分數,好的分數應該低於 0.1。動畫示範一個網格版面,某個元素因為圖片載入而突然變大,周圍元素就被迫讓位,每一次讓位都累加進分數。

網格動畫中元素變大、周圍被推開的那一格,是「位移如何累加」最直觀的畫面
6:32 · 網格動畫中元素變大、周圍被推開的那一格,是「位移如何累加」最直觀的畫面
承上 承上:上一段留下「內容出現時把別人擠開」這個新問題,這一段給它一個名字與一個分數——layout shift 與 CLS。

推理因為上一段的修法只保證內容早點出現、沒保證出現得安分,所以作者接著換一個面向:版面穩定。他先用一個人人有共鳴的場景定義問題——你正在讀一段文字,上方的圖片或廣告載入,文字被擠走——再說明 CLS 把所有這類位移加總成分數,好的分數低於 0.1。接著用網格動畫演示機制:某個元素因為圖片載入而突然變大,周圍元素被迫讓位,接著另一個元素從正方形變長方形又推一次,每一次讓位都累加進分數。

AI 補充動畫(畫面下方的字幕就寫「1. Square grows to 2×2 → others reflow」)把機制講對了,但「把所有位移加總」是簡化的說法,有兩個修正值得知道。第一,只有「非預期」的位移才算:使用者互動後 500 毫秒內發生的移動被視為預期而豁免,所以點按鈕展開手風琴、按下去載入更多內容都不會被罰——這也是為什麼「先讓使用者按了才長出來」是合法的設計策略。第二,2020 年 6 月起 CLS 已經不是整頁生命週期一路加總,而是改用 session window:把位移切成一段段(間隔超過 1 秒或整段超過 5 秒就換一段),最後取分數最高的那一段。這個修正是為了不讓長時間停留的頁面(例如無限捲動的動態牆)被無止境地累加成天文數字。至於分數怎麼算,每次位移的分數是「受影響區域佔視窗的比例」乘上「移動距離佔視窗的比例」,兩個因子都以視窗為分母——記住這點,下一段作者示範改變視窗大小分數就變的現象才不會顯得神秘。

layout shift

版面位移

已經畫出來的可見元素,在使用者沒有主動操作的情況下改變了它在畫面上的位置。

判定的是「已渲染元素的起始位置變了」,所以新元素直接出現在空白處不算位移,但它把既有內容推開就算。單次位移的分數 = 受影響區域佔視窗的比例 × 移動距離佔視窗的比例,兩者都以視窗為分母。使用者互動後 500 毫秒內的位移會被標記為 hadRecentInput 而不計入。DevTools 的 Performance 面板有 Layout shifts 分頁,可以逐次看到是哪個元素在什麼時間點推了誰。

相關術語: Cumulative Layout Shift (累加成它)、reflow (由其造成)、viewport (以其為分母)

出處:第 7 段「CLS:版面為什麼會亂跳」

reflow

重新排版

瀏覽器因為元素尺寸或位置改變,重新計算受影響區域幾何位置的過程。

又稱 layout。它和 repaint(重繪)不同:改顏色只需重繪,改寬高則會觸發重排,而重排通常會連帶重繪,成本高得多。重排本身是必要的正常行為,會不會變成使用者看得到的 layout shift,取決於它有沒有把已經畫出來的內容推走。在 JavaScript 裡交錯讀取 offsetHeight 與寫入樣式會造成強制同步重排(layout thrashing),是另一個常見的效能陷阱。

相關術語: layout shift (造成)、DOM (作用於)

出處:第 7 段「CLS:版面為什麼會亂跳」

留給下一段 動畫裡「元素突然變大」的原因被歸給「尺寸事先不知道」,但程式碼裡的「尺寸不知道」到底長什麼樣、又是怎麼被寫出來的——下一段回到編輯器看現場。

8. before:圖片沒寫寬高就會擠壓版面 6:48–8:22

before 版的 img 沒有指定 width 與 height,甚至沒有 src,只有 data-source,靠 JavaScript 延遲插入。頁面初次執行時瀏覽器不知道圖片要佔多少空間,就不會預留位置,等圖片載入時才硬插進版面,下方內容整個被往下擠。作者也示範同一個頁面改變視窗大小後再重整,CLS 分數會不同,因為位移只算視窗內、且圖片相對視窗的比例變大了。

before 的 img 標籤:沒有寬高、只有 data-source,說明「瀏覽器無從得知尺寸」的根源
7:27 · before 的 img 標籤:沒有寬高、只有 data-source,說明「瀏覽器無從得知尺寸」的根源
瀏覽器裡圖片突然撐開、下方內容被擠走的瞬間,是 CLS 的現場證據
7:46 · 瀏覽器裡圖片突然撐開、下方內容被擠走的瞬間,是 CLS 的現場證據
承上 承上:上一段把病因歸結為「尺寸事先不知道」,這一段就打開 cls-before.html,看這個「不知道」在程式碼裡具體是哪幾個字。

推理因為上一段已經指出位移的根源是尺寸未知,所以作者回到編輯器逐項確認這個條件是怎麼被製造出來的: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

intrinsic size

內在尺寸

圖片或影片檔案本身自帶的原始寬高,瀏覽器要下載到檔頭才會知道。

版面之所以會跳,本質是「內在尺寸只有下載後才知道,但排版在下載前就要做」這個時間差。在 HTML 上寫 width 與 height,等於預先把這個資訊告訴瀏覽器,讓它不必等下載就能算出要留多大的空間。對於尺寸真的不固定的內容(使用者上傳的圖、第三方嵌入),做法是改用 CSS 的 aspect-ratio 或固定高度的容器先把位置佔住。

相關術語: aspect ratio (由其算出)、layout shift (未知時造成)

出處:第 8 段「before:圖片沒寫寬高就會擠壓版面」

data-src

自訂資料屬性存網址

把圖片網址暫存在 data-* 屬性裡、之後由 JavaScript 搬到 src 上的延遲載入寫法。

這是 loading="lazy" 普及之前的手寫 lazy loading 慣用手法,今天多半已可用原生屬性取代。它的副作用正是這一段的重點:因為網址不在 src 上,瀏覽器的 preload scanner 掃不到,圖片一定要等 JavaScript 跑完才開始下載,因此必然落在首次繪製之後。若同時又沒寫寬高,就是版面位移的標準配方。若真的要用,至少要在容器上先把空間預留好。

相關術語: preload scanner (使其失效)、intrinsic size (使其更晚知道)

出處:第 8 段「before:圖片沒寫寬高就會擠壓版面」

留給下一段 病因確認在「瀏覽器不知道要留多少空間」,那最小的處方應該就是「先告訴它」——下一段驗證這個猜想到底夠不夠。

9. after:寫上寬高,CLS 歸零 8:22–10:11

改善版只做一件事:幫 img 加上 width 與 height。瀏覽器就能算出正確的長寬比、立刻預留空間,實測 CLS 是 0。即使把網路節流到 fast 4G 拖慢圖片載入,CLS 依然是 0,因為版面在圖片載入完成前就已經知道要留多少位置。重點是:只要內容可能晚點才載入,就先讓瀏覽器知道它的尺寸。段末作者順帶介紹自己的 Front-End System Design Essentials 課程。

after 的 img 標籤帶著 width / height,與第 8 段的 before 標籤形成一組最小對照
8:37 · after 的 img 標籤帶著 width / height,與第 8 段的 before 標籤形成一組最小對照
承上 承上:上一段留下的猜想是「先告訴瀏覽器尺寸就夠了」,這一段用 cls-after.html 驗證它,並把 CLS 壓到 0。

推理因為上一段把病因鎖定在資訊缺口而不是載入速度,所以作者的處方也就對準資訊本身:在 img 上補回 width 與 height,瀏覽器據此算出長寬比、在圖片還沒下載完之前就把空間預留好。他還做了一個很漂亮的反證——把網路節流到 fast 4G 讓圖片載得更慢,CLS 依然是 0。這證明版面穩定與載入快慢是兩件獨立的事:慢不會造成位移,資訊不足才會。整章的收束因此是:只要內容可能晚點才到,就先讓瀏覽器知道它的尺寸。

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 課程,屬於業配段落,與推理無關。

aspect ratio

長寬比

元素寬與高的比例;瀏覽器只要知道它,就能在內容還沒到之前先算出要保留多大的區域。

從 2019 年起,主流瀏覽器會把 <img> 上的 width 與 height 屬性換算成內部的 aspect-ratio,因此「寫上寬高」和「響應式縮放」不再互斥:CSS 用 width:100%; height:auto,空間照樣預留正確。CSS 也有獨立的 aspect-ratio 屬性,適合尺寸未知的嵌入內容(影片、iframe、使用者上傳圖)。要注意屬性值要填圖片的真實比例,填錯會讓預留的空間和實際不符,反而製造另一次位移。

相關術語: intrinsic size (由其推得)、layout shift (用於消除)

出處:第 9 段「after:寫上寬高,CLS 歸零」

network throttling

網路節流

DevTools 用來模擬較慢連線(如 Fast 4G)的開關,讓開發機也能重現真實使用者的網路條件。

它是效能實驗裡的控制變因工具:作者刻意把圖片載得更慢,卻讓 CLS 維持 0,藉此證明位移與載入速度無關。使用時通常要一併勾選停用快取,否則第二次載入就沒有對照價值。它模擬的是頻寬與延遲,無法完全重現真實行動網路的抖動與封包遺失,所以結論仍要靠真實使用者資料驗證。與它成對的另一個開關是 CPU 節流,下一章談 INP 時會用到。

相關術語: CPU throttling (成對工具)、Chrome DevTools Performance panel (位於其中)

出處:第 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 逐行對照
留給下一段 載入(LCP)與版面(CLS)都收束了,但兩者都發生在使用者「還在看」的階段;等使用者真的動手按下去,瓶頸會換成另一種——這留給最後一個指標。

10. INP:主執行緒被長工作卡住 10:11–11:17

INP 量的是使用者操作到畫面下一次更新之間的時間,理想應低於 200 毫秒。問題情境是按鈕觸發一大堆運算:因為瀏覽器的 JavaScript 是單執行緒,這些重活會卡住主執行緒,瀏覽器既無法更新 UI 也無法回應其他互動。使用者按了按鈕卻什麼都沒發生,直到運算結束畫面才更新,INP 分數自然很差。

主執行緒被長工作佔滿、UI 無法更新的示意動畫,是理解 INP 成因的核心畫面
11:00 · 主執行緒被長工作佔滿、UI 無法更新的示意動畫,是理解 INP 成因的核心畫面
承上 承上:上一段留下「使用者真的動手之後瓶頸會換一種」,這一段就把場景推進到互動階段,並指出新的瓶頸是主執行緒。

推理因為前面兩個指標都在處理「畫面呈現」,而使用者一旦開始操作就換成另一組期待,所以作者把 INP 定義成互動到畫面下一次更新之間的時間,理想低於 200 毫秒。接著他用同一套「拆時序找瓶頸」的手法:按鈕觸發一大堆運算,而瀏覽器裡的 JavaScript 是單執行緒,這些重活一旦跑起來就佔住主執行緒,瀏覽器既無法更新 UI 也無法回應其他互動。於是使用者按了按鈕卻什麼都沒發生,要等運算結束畫面才更新——這正是差勁 INP 分數的來源。

AI 補充作者的定義是對的,但少講了一層會影響你怎麼讀數字:INP 不是某一次互動的耗時,而是把整次造訪的所有互動延遲蒐集起來,取近乎最差的那一次來代表這個頁面(互動很多時會捨去少數極端值)。所以「有一次很卡」就足以拖垮分數,平均值再好也沒用——這也是為什麼下一段作者狂點按鈕,數字會那麼難看。另外,單次互動延遲其實由三段組成:input delay(事件排到隊之前的等待)、processing time(事件處理器本身執行)、presentation delay(處理完到瀏覽器實際畫出下一幀)。作者示範的「主執行緒被長工作佔住」主要放大的是第一段和第三段,而不是事件處理器本身寫得慢——這個區分很重要,因為它決定了下一段的修法為什麼是「把工作切開讓出時間」而不是「把運算寫得更快」。最後補充一個常被忽略的細節:把重運算包進 async/await 或 Promise 並不能解決問題,它們仍在同一條主執行緒上排隊。

main thread

主執行緒

瀏覽器分頁裡負責執行 JavaScript、計算樣式、排版與繪製的那一條執行緒。

所有這些工作共用同一條線,所以一段長時間執行的 JavaScript 會連帶讓排版與繪製都無法進行——畫面凍住的根本原因就在這裡。它是分頁層級的資源,跨分頁不互相影響。要真正把運算移開,只能交給 Web Worker(另一條執行緒,但不能碰 DOM);用 Promise 或 async/await 只是換個排隊方式,工作照樣落在主執行緒上。

相關術語: long task (被其佔用)、Interaction to Next Paint (阻塞則惡化)

出處:第 10 段「INP:主執行緒被長工作卡住」

long task

長工作

在主執行緒上連續執行超過 50 毫秒、期間瀏覽器無法回應任何互動的一段工作。

50 毫秒是 W3C Long Tasks API 訂的界線,背後的理由是:只要讓出的間隔夠短,使用者就感覺不到延遲。長工作期間所有輸入事件都只能排隊,因此它同時傷害 INP 的 input delay 與 presentation delay 兩段。在 DevTools 的效能錄影裡,長工作會被標成帶紅色三角形的灰色方塊,是找互動瓶頸最快的入口。解法從來不是把它寫快一點,而是把它切開。

相關術語: main thread (佔用它)、task chunking (解法)

出處:第 10 段「INP:主執行緒被長工作卡住」

input delay

輸入延遲

使用者操作發生後,事件處理器真正開始執行前的等待時間。

它是單次互動延遲三段(input delay、processing time、presentation delay)裡的第一段,也是主執行緒被佔住時膨脹得最誇張的一段——事件已經產生,卻只能排在長工作後面。舊指標 FID 量的就只有這一段,這也是它被 INP 取代的原因:處理器跑得再久、畫面更新得再晚,FID 都看不到。優化它靠的不是加快處理器,而是讓主執行緒保持有空檔。

相關術語: Interaction to Next Paint (其組成之一)、long task (被其拉長)

出處:第 10 段「INP:主執行緒被長工作卡住」

留給下一段 「使用者按了卻沒反應」目前還只是動畫上的說法,在開發機那種快 CPU 上根本重現不出來——下一段要想辦法把這個現象放大到看得見。

11. 20 倍降速實測:卡住 vs 有反應 11:17–12:35

作者把 CPU 節流調成 20 倍慢來放大問題:before 頁面按下 process data 後,再怎麼點計數器都沒有反應,DevTools 的 INP 明顯偏長。after 頁面同樣是 20 倍慢的 CPU,處理進行中計數器仍然可以按、畫面持續更新,INP 分數也比較好。

before 的畫面:連點計數器完全沒反應、INP 數字偏長,這個對比只有看畫面才成立
12:11 · before 的畫面:連點計數器完全沒反應、INP 數字偏長,這個對比只有看畫面才成立
承上 承上:上一段的卡頓在開發機上重現不出來,這一段就用 DevTools 把 CPU 節流成 20 倍慢,把現象放大到肉眼可見。

推理因為上一段的因果鏈需要一個看得見的現場,而現代開發機太快、長工作一閃就過,所以作者先建立控制變因: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 手機的真實處境;在開發機上跑不出問題卻在使用者端災難,正是效能問題最常見的盲區——所以節流不是作弊,而是把你的機器校正到使用者的水準。

CPU throttling

CPU 節流

DevTools 用來人為降低 JavaScript 執行速度(如 4×、20× 慢)以模擬低階裝置的開關。

它按倍率拖慢主執行緒上的所有運算,用途是把開發機校正到真實使用者的裝置水準——多數使用者的手機比開發者的筆電慢一個數量級,20× 大致對應入門款 Android。它只影響 CPU,不影響網路,所以通常要和網路節流搭配才能重現完整情境。要注意倍率是模擬而非精確對應某款機型,適合做 before/after 的相對比較,不適合當成絕對規格宣稱。

相關術語: network throttling (成對工具)、long task (使其更明顯)

出處:第 11 段「20 倍降速實測:卡住 vs 有反應」

留給下一段 after 頁面在同樣 20 倍慢的 CPU 上卻按得動,運算量並沒有變少——那它到底改了什麼,這個謎題留給下一段揭曉。

12. 修法:切成小塊並交還控制權 12:35–13:40

改善的做法不是一次做完所有工作,而是把任務切成小塊:按下按鈕就先立刻更新 UI 顯示「處理中」,處理一小部分後把控制權交還瀏覽器,可以用 setTimeout 或分塊迴圈實作。因為週期性讓出控制權,瀏覽器有機會重繪畫面、回應其他互動,使用者立刻看到回饋。範例裡每塊只處理 500 筆,比一口氣跑一萬筆快得多。關鍵想法:盡可能讓主執行緒保持空閒,重活要切小。

after 的分塊處理程式碼(每塊 500 筆、yield 回瀏覽器),是這個修法的具體樣貌
13:18 · after 的分塊處理程式碼(每塊 500 筆、yield 回瀏覽器),是這個修法的具體樣貌
承上 承上:上一段留下的謎題是「運算量沒變、為什麼 after 按得動」,這一段打開程式碼給出答案——工作被切成小塊,每塊之間把控制權還給瀏覽器。

推理因為上一段確認差別不在運算量,所以作者把答案指向執行的形狀而非大小:不再一次做完,而是切成小塊。按鈕一被按下就先立刻更新 UI 顯示「處理中」,接著處理一小部分,再把控制權交還瀏覽器,用 setTimeout 或分塊迴圈實作。因為週期性讓出控制權,瀏覽器就有空檔重繪畫面、回應其他互動,使用者立刻看到回饋。範例裡每塊只有 500 筆,遠比一口氣跑一萬筆友善。整章因此收束成:盡可能讓主執行緒保持空閒,重活要拆小。

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

task chunking

工作分塊

把一段長時間的運算切成許多小塊分批執行,讓瀏覽器能在塊與塊之間插入繪製與事件處理。

重點不在把總時間縮短(切開之後總時間通常還略增),而在把「一段連續佔用」變成「許多短暫佔用」,讓每個空檔都足以回應一次互動。挑塊大小的原則是讓每塊控制在 50 毫秒以內,也就是不製造長工作。實作上除了遞迴 setTimeout,也可以用 for 迴圈搭配時間預算檢查,或直接改用支援優先權的排程 API。若運算不需要碰 DOM,改丟 Web Worker 是更徹底的做法。

相關術語: long task (用於拆解)、yielding to the main thread (依賴)

出處:第 12 段「修法:切成小塊並交還控制權」

yielding to the main thread

把控制權交還主執行緒

在一段運算中途主動結束目前任務、讓瀏覽器有機會繪製與處理事件,之後再排下一段繼續。

常見寫法是 setTimeout(fn, 0)——這個 0 的意思是「排到任務隊尾」而不是「等 0 秒」,因為只有巨集任務之間瀏覽器才會插入繪製;用 Promise.resolve().then() 是微任務,會在同一輪清空,不會讓出繪製機會,這是初學者最常踩的坑。現代瀏覽器提供了專用的 scheduler.yield(),它讓出之後仍保有優先權,避免被無關工作插隊。判斷有沒有真的讓出,看 DevTools 錄影裡那段灰色方塊有沒有被切開就知道。

相關術語: task chunking (其手段)、main thread (作用於)

出處:第 12 段「修法:切成小塊並交還控制權」

Web Worker

背景執行緒

瀏覽器提供的另一條 JavaScript 執行緒,可以跑重運算而完全不佔用主執行緒。

它與主執行緒透過訊息傳遞溝通,代價是不能直接存取 DOM,資料進出也需要複製(或用 Transferable/SharedArrayBuffer 避開複製成本)。適合純運算:解析大型 JSON、影像處理、加解密、資料表排序與彙總。分塊與 Worker 不是二選一——需要中途更新畫面的流程用分塊,能整包搬走的重運算用 Worker,兩者常一起用。

相關術語: main thread (分擔其負載)、task chunking (替代方案)

出處:第 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()
留給下一段 三個指標各自的機制、病灶與處方都走完了,剩下的是把三條線收在一起——總結收束前的最後一步,看它們是不是同一個道理的三種說法。

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

📄 全部影片 · 主題區: 網頁效能