前端效能的三把量尺:Performance API、Chrome Performance、React Profiler

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

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

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

1. Outline

  1. 起點 · Frontend System Design: Performance API, Chrome, React Profiler
    從「頁面很慢」這句沒有資訊量的話出發,依序引入三個測量工具,每個工具都由前一個工具答不出來的問題逼出來,最後收成「先量再優化」的行動守則。

2. YouTuber 的思維推導

Frontend System Design: Performance API, Chrome, React Profiler

I Code It · 13m12s · 字幕 en · vision=on (input 指定 vision=true;影片有 code、console.table 結果、Chrome performance 火焰圖與 React Profiler 輸出,作者多次指著畫面說「you can see here」,讀圖才看得懂數字對照。) · YouTube

預估 vs 實際耗時
階段預估實際
fetch 6s 2s
segment 56s 5m00s
shot 37s 4s
analyze 5m00s 10m08s
render 5s –

作者的起點是一句沒有資訊量的抱怨:「這頁很慢」。他先把它拆成三個可回答的問題——有多慢、哪裡慢、跟什麼比——藉此推出全片的前提:沒有測量的優化只是猜測,所以要先建立 baseline,改完再量一次。接著他不是一次丟出三個工具,而是讓每個工具都由前一個工具的**不足**逼出來:先用瀏覽器內建、零安裝的 performance.now() 量出「這個操作花多久」,發現一個總數看不出該改哪一步,於是換 mark / measure 把操作拆成有名字的 search / filter / aggregate;拆完仍然只知道「多久」而不知道「為什麼」,於是打開 Chrome Performance 錄下整段互動,在 main thread 上看見 long task 與 simulateHeavyWork,把「一個數字」變成「數字的出處」;再追問「那 React 自己呢」,用 React Profiler 只量在意的那棵子樹,得到約 30 毫秒。

最後他把 30 毫秒與先前量到的約 400 毫秒並排,直接推翻「大概是 React 慢」的直覺,證明三個工具誰也取代不了誰、各給拼圖的一塊。收尾時他補上使用守則(profiling 有 overhead,數字當相對比較用),並把「找到瓶頸之後怎麼改」明確劃出這支影片的範圍之外。整條推導其實是一條解析度由粗到細的路線:先確認有問題、再定位在哪、最後才進到框架內部。

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

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

  1. 「頁面很慢」不是資訊 → 既然結論是「先量」,馬上冒出來的問題就是「用什麼量」,而且那個東西必須便宜到可以隨時重跑:不用安裝、不綁框架、按一下就有數字。
  2. Performance API:瀏覽器內建的碼錶 → 碼錶的用法講完了,但還沒真的按下去:接下來要把它套到一個真實的、會慢的操作上,看看第一個數字長什麼樣,以及那個數字本身到底算不算解決了問題。
  3. performance.now 量出第一條 baseline → 一個總數有了,但畫面上的 analyzeLogs 裡其實串著 searchLogs、filterLogs、aggregateLogs 三個步驟——總數沒辦法告訴我們該去改哪一個,碼錶必須拆細。
  4. mark / measure:把一個操作拆成有名字的步驟 → 現在知道慢的是 search 這一步了,但手上仍然只有「哪一步、多久」——那 8 毫秒(換成真實資料量就是幾百毫秒)究竟是花在 JavaScript 計算、layout 還是畫面繪製,完全看不出來。
  5. Performance API 的天花板:知道多久,不知道為何 → 剩下的問題變成:有沒有一個工具,能把整段互動期間瀏覽器做的每一件事都錄下來,而不是只量我親手夾住的那一段程式碼?
  6. Chrome Performance:錄下整段互動 → 錄是錄下來了,但一份十幾秒、疊了好幾條軌的時間軸本身也還不是答案——得從裡面把那四百毫秒定位到某一條軌、某一個函式上。
  7. 火焰圖指出:時間全花在 main thread → main thread 上的 JavaScript 被指認成主嫌了,但同一張截圖左側還有 Scheduler 與 Components 兩條 React 專屬的軌沒有讀——React 自己渲染那些結果花了多少時間,仍是一片空白,也就無法排除「是 React 渲染太多元件」這個可能。
  8. React Profiler:只量你在意的那棵子樹 → 手上多了 30 毫秒這個數字,但單獨看它沒有任何意義——它得跟先前量到的分析時間並排,才能回答「該不該去優化 React」。
  9. 三個數字擺在一起才有結論 → 結論看起來乾淨俐落,但它整個建立在這些數字可信的前提上;而測量本身會不會影響被測對象、開發模式下的數字能不能當真,到這裡都還沒有交代。
  10. Profiler 的數字怎麼讀才對 → 三個工具、三種數字、以及讀數字的規矩都齊了,剩下的是把它們收成一條可以照著做的行動守則。
  11. 總結:先量,再決定要優化什麼

3. 逐段說明

Frontend System Design: Performance API, Chrome, React Profiler

1. 「頁面很慢」不是資訊 0:00–1:38

作者指出效能是前端最神祕的題目:大家說「這頁很慢」「我們有效能問題」,但沒回答有多慢、哪裡慢、跟什麼比。沒有這些答案,優化就只是猜測——切 code、搬 web worker、換演算法,改完也不知道有沒有變好,甚至可能更糟。結論是先測量、建立 baseline,改完再測一次。他要示範三種測法,各自回答不同的問題。

承上 前情提要裡「你已經知道好幾種效能優化手段(切 code、搬 web worker、換演算法),卻沒有辦法證明它們有沒有用」這個背景,正是作者的起點。他刻意不從任何一種手段講起,而是回頭質疑「慢」這個字本身。

推理因為讀者手上通常只有「這頁很慢」這句話,所以作者接著做的不是給工具,而是把這句話逼成三個必須有答案的問題:有多慢?哪一部分慢?跟什麼比?只要這三個問題還空著,任何優化動作事後都無法判定成敗——他甚至指出可能改到更慢卻不自知。由此推出全片的地基:先測量、建立 baseline,改動之後再測一次。

AI 補充三個問題裡最容易被忽略的是第三個「跟什麼比」。它的意思不是「跟別家網站比」,而是「跟改動前的自己比」,這正是 baseline 的定義。要讓兩次數字可比,測量必須是可重跑的同一段程式、同一批資料、同一台機器、同一種 build,否則差異可能全來自環境。作者這裡跳過了一步值得補上:既然流程是「量 → 改 → 再量」,測量本身就必須夠便宜、夠隨手可得,否則沒有人會真的跑第二次——這個隱含條件直接決定了他挑工具的順序。

術語:performance optimizationcode splittinglazy loading

baseline

基準線

優化前先量到的一組數字,之後所有改動都跟它比才知道有沒有變好。

baseline 不是「一個數字」而是「一次可重複的測量」:同一段程式、同一批資料、同一台機器、同一種 build。少了任何一個條件,前後兩次的差可能只是環境噪音。實務上常見的錯誤是改完才想到要 baseline,這時舊版本已經不在手邊,只能憑印象說「感覺快多了」。另一個誤解是把 baseline 當成目標值;它其實只是原點,用來衡量位移。

相關術語: performance optimization (前提)

出處:第 1 段「「頁面很慢」不是資訊」

performance optimization

效能優化

為了讓頁面更快而做的改動,例如換演算法、搬走運算、減少載入量。

作者的立場是:沒有測量的效能優化本質上是賭博。真正的困難不在於不知道有哪些手段,而在於不知道該用哪一個——同樣一個「慢」,瓶頸在演算法、在網路、在渲染,對應的手段完全不同,用錯了不但沒效,還會增加程式碼複雜度。所以他把整支影片限縮在「測量」這一半。

相關術語: baseline (必須先有)

出處:第 1 段「「頁面很慢」不是資訊」

code splitting

程式碼分割

把打包好的 JavaScript 切成多個檔案,讓頁面只先載入當下需要的那塊。

常和 lazy loading 一起出現:切開是前提,延後載入是行為。它處理的是「載入太多」這一類問題,對「已經載入完、按下按鈕之後很慢」這種運算瓶頸幾乎沒有幫助——這正是作者拿它當「亂猜」例子的原因。

相關術語: lazy loading (常搭配使用)

出處:第 1 段「「頁面很慢」不是資訊」

lazy loading

延遲載入

等到真正需要時才去載入某段程式碼、圖片或資料,而不是一開始就全部載入。

延後的是「載入」不是「執行成本」:該跑的運算還是會跑,只是換了時間點。如果瓶頸是使用者按下按鈕後那幾百毫秒的計算,lazy loading 反而可能讓那一刻變得更慢(要先下載再執行)。

相關術語: code splitting (以它為前提)

出處:第 1 段「「頁面很慢」不是資訊」

web worker

背景執行緒

瀏覽器提供的獨立執行緒,可以把耗時運算搬離主執行緒,讓畫面不被卡住。

worker 有自己的 JavaScript 環境,碰不到 DOM,跟主執行緒之間只能靠訊息傳遞(結構化複製)溝通,所以搬過去是有成本的:資料量大時序列化本身就會吃掉一部分好處。它不會讓運算變快,只是換一條執行緒跑,讓 UI 保持可互動——這也是為什麼一定要先量過,才知道值不值得搬。

相關術語: performance optimization (一種手段)

出處:第 1 段「「頁面很慢」不是資訊」

留給下一段 既然結論是「先量」,馬上冒出來的問題就是「用什麼量」,而且那個東西必須便宜到可以隨時重跑:不用安裝、不綁框架、按一下就有數字。

2. Performance API:瀏覽器內建的碼錶 1:38–2:13

第一個工具是 Performance API。作者喜歡它的理由很單純:已經內建在瀏覽器裡,不用安裝、不依賴 React 或任何框架。它回答的是最基本的一個問題——這個操作花了多久。心智模型就是碼錶:開始前按下、結束時按停,中間的差就是 duration。

承上 上一段留下的問題是「用什麼量,而且要便宜到可以隨時重跑」。作者這一段的答案就是 Performance API:已經內建在瀏覽器裡,不用安裝、不依賴 React 或任何框架,剛好滿足「隨手可得、可重複」這兩個條件。

推理因為上一段把需求定成「先量、而且要能一再重跑」,所以作者接著挑的是成本最低的那個工具,而不是功能最強的。他用一句話把它的能力範圍框死——它只回答「這個操作花了多久」——再用碼錶的比喻把使用方式壓縮成兩個動作:開始前按下、結束時按停,中間的差就是 duration。

AI 補充「Performance API」不是單一函式,而是掛在 window.performance 上的一整組介面:取時間的 now()、打點與命名的 mark() / measure()、把結果取回來的 getEntries 系列,以及非同步監聽用的 PerformanceObserver。碼錶比喻很好用,但有一個藏起來的前提:被夾住的那段程式必須是同步的。一旦中間出現 await、setTimeout 或任何非同步等待,「按停」的那一行就不在你以為的位置上,量到的會是「這段同步程式碼跑完為止」,而不是「這件事做完為止」。這個限制之後決定了它能量什麼、不能量什麼。

Performance API

瀏覽器效能量測 API

瀏覽器內建的一組時間量測介面,掛在 window.performance 上,不需安裝、不綁任何框架。

它由多個 W3C 規格拼起來(High Resolution Time、User Timing、Performance Timeline、Navigation/Resource Timing…),所以介面看起來有點雜。實務上最常用的只有三個:now() 取時間、mark()/measure() 打點命名、getEntriesByType() 取回結果。因為它是瀏覽器層級的東西,Node.js 也提供了對應的 perf_hooks,寫法幾乎一樣。要注意它量的是「牆上時間」,不是 CPU 時間:如果分頁被切到背景或執行緒被搶佔,數字會一起被拉長。

相關術語: duration (產出)

出處:第 2 段「Performance API:瀏覽器內建的碼錶」

duration

執行時間

一段操作從開始到結束經過的時間,也就是碼錶按停與按下之間的差。

duration 是「經過了多久」而不是「用掉多少 CPU」,兩者在單執行緒的瀏覽器裡常常接近,但只要中間有等待(網路、非同步、被別的 task 插隊)就會拉開。另外它是一個純量:它告訴你長度,不告訴你組成,這正是作者後面要用別的工具補上的那一塊。

相關術語: baseline (構成)

出處:第 2 段「Performance API:瀏覽器內建的碼錶」

留給下一段 碼錶的用法講完了,但還沒真的按下去:接下來要把它套到一個真實的、會慢的操作上,看看第一個數字長什麼樣,以及那個數字本身到底算不算解決了問題。

3. performance.now 量出第一條 baseline 2:13–3:30

用前一支影片的 log analyzer 當例子:使用者輸入關鍵字後,前端要分析約五萬筆 log。作法很直接——呼叫 analyzeLogs 之前用 performance.now() 取一個高解析時間戳,函式回來後再取一個,兩者相減就是 duration。作者強調這個數字本身解決不了問題,但它給了更重要的東西:baseline。之後把工作搬進 web worker 或改演算法,可以用同一段測量再跑一次做對照,不再靠猜。

performance.now() 前後包住 analyzeLogs 的實際寫法,看過這幾行才知道「碼錶」在程式碼裡長什麼樣。
2:43 · performance.now() 前後包住 analyzeLogs 的實際寫法,看過這幾行才知道「碼錶」在程式碼裡長什麼樣。
在瀏覽器點下按鈕後印出的 duration 數字,這是文中所說的 baseline 本體。
3:00 · 在瀏覽器點下按鈕後印出的 duration 數字,這是文中所說的 baseline 本體。
承上 上一段留下的是「把碼錶真的按下去看看數字」。這一段作者就拿前一支影片的 log analyzer 當受測對象——使用者輸入關鍵字後要在前端分析五萬筆 log——把「按開始/按停止」直接換成前後兩次 performance.now()。

推理因為上一段的心智模型只剩下開始與停止兩個動作,所以作者接著找的是最小的實作:呼叫 analyzeLogs 之前取一個高解析時間戳、函式回來後再取一個、相減得到 duration。他隨即補上一句關鍵的自我修正——這個數字本身解決不了問題——把重點從「數字」轉到「這是一條可以重跑的 baseline」:之後不論搬進 web worker 還是改演算法,都能用同一段測量再跑一次做對照。

AI 補充畫面上的程式碼把這件事寫得很乾淨:`const start: DOMHighResTimeStamp = performance.now()`、`const result = analyzeLogs(logs, query)`、`const duration = performance.now() - start`,而檔案開頭的註解已經先招認了限制——「You learn *how long* it took — not *which stage* is slow」。有兩點作者沒說:第一,為什麼是 performance.now() 而不是 Date.now()——前者是單調遞增、以頁面載入為原點的高解析時鐘,不受系統時間校正影響,用 Date.now() 在跨時區同步或 NTP 校時的瞬間甚至可能量出負值;第二,瀏覽器基於 Spectre 類的側通道疑慮會把這個時鐘的解析度刻意調粗(毫秒以下只到微秒等級),所以量單次、極短的操作時要跑多次取中位數。至於瀏覽器裡印出的 `Log analysis took 9.50ms`,那是 demo 用的 20,000 筆資料,數字小不代表方法沒用:baseline 的價值在於「同一段測量能再跑一次」,不在絕對值大小。

performance.now()

高解析時間戳函式

回傳從頁面載入起算、以毫秒為單位的高解析時間,用來夾住一段程式碼算出耗時。

它的原點是該文件的 time origin(大致是頁面開始載入的那一刻),而且保證單調遞增,所以兩次相減一定是非負數。相對地 Date.now() 是系統牆鐘,會被使用者調時間或 NTP 校時影響。要跨執行緒或跨頁面對齊時間軸時,要記得每個 context 的 time origin 不同,直接比大小沒有意義。

相關術語: DOMHighResTimeStamp (回傳型別)、duration (相減得到)

出處:第 3 段「performance.now 量出第一條 baseline」

DOMHighResTimeStamp

高解析時間戳型別

以毫秒為單位、可帶小數的時間值型別,performance.now() 與各種 performance entry 的時間欄位都是它。

名字看起來很唬人,其實就是一個 double。「高解析」指的是它可以帶小數(理論上到微秒),而不是像 Date.now() 只到整數毫秒。實際解析度由瀏覽器決定並且會被刻意調粗以防側通道攻擊,跨來源隔離(COOP/COEP)的頁面才拿得到最細的解析度。畫面上 TypeScript 把它標出來,是提醒你它是時間點不是時間長度。

相關術語: performance.now() (由它回傳)

出處:第 3 段「performance.now 量出第一條 baseline」

留給下一段 一個總數有了,但畫面上的 analyzeLogs 裡其實串著 searchLogs、filterLogs、aggregateLogs 三個步驟——總數沒辦法告訴我們該去改哪一個,碼錶必須拆細。

4. mark / measure:把一個操作拆成有名字的步驟 3:30–5:44

單一操作用 performance.now 就夠,但實務上一個操作常包含好幾個小步驟——log analyzer 會先 search、再 filter、最後 aggregate。作者改用 performance.mark 在每個步驟前後打點(searchStart / searchEnd),再用 performance.measure 把兩點之間命名為 search,filter 與 aggregate 比照辦理,最後用 console.table 印成一張表。示範中 search 約 8 毫秒、filter 0.1、aggregate 0.2(因為 demo 資料量小;完整例子有一萬五千筆就明顯得多)。他最欣賞的是每次測量都有一個有意義的名字,比一串匿名時間戳好讀太多。

mark / measure 的成對寫法:start 與 end 打點、再 measure 命名,這是這一段的核心語法。
4:10 · mark / measure 的成對寫法:start 與 end 打點、再 measure 命名,這是這一段的核心語法。
console.table 印出的 search / filter / aggregate 三列與各自 duration,「有名字的測量」的價值在這張表上才看得出來。
5:03 · console.table 印出的 search / filter / aggregate 三列與各自 duration,「有名字的測量」的價值在這張表上才看得出來。
承上 上一段結束在「總數看不出該優化哪一步」。這一段作者就把一支碼錶換成三對打點:既然 analyzeLogs 裡本來就有 search、filter、aggregate 三個階段,那就分別量它們。

推理因為上一段只拿到一個總數,所以作者接著要的是「拆解」而不是「更準」。他用 performance.mark() 在每個步驟前後各打一個點(search-start / search-end),再用 performance.measure() 把兩點之間命名為 search,filter 與 aggregate 比照辦理,最後用 console.table() 一次印成表。他最看重的不是精度而是可讀性:每筆測量都有一個有意義的名字,比一串匿名時間戳好懂太多。

AI 補充畫面上完整的寫法是 `performance.mark('search-start')` → 執行 → `performance.mark('search-end')` → `performance.measure('search', 'search-start', 'search-end')`,最後用 `performance.getEntriesByType('measure')` 把所有命名區段撈回來、map 成 name 與 duration 兩欄餵給 console.table()。這裡有一個作者沒點破、但決定了 measure 價值的差別:performance.now() 的結果只活在你的區域變數裡,而 measure 的結果會留在瀏覽器的 performance timeline 緩衝區裡,因此瀏覽器的效能記錄工具也讀得到同一批命名區段——同一份標註同時服務 console 和錄製工具。另外兩個實務坑:mark 與 measure 會不斷累積,同一顆按鈕多按幾次後 getEntriesByType 會撈到愈來愈多筆,需要 clearMarks() / clearMeasures() 清掉;而且這些字串名稱要自己維護,打錯不會報錯,只會安靜地少一列。至於表上的數字——search 8.20ms、filter 0.10ms、aggregate 0.20ms,總計 8.5ms——絕對值不重要,重要的是比例:時間有 96% 落在 search,另外兩步加起來不到 4%,這種一眼可見的傾斜才是拆解真正買到的東西。

術語:performance.getEntriesByType()

performance.mark()

打點

在時間軸上留下一個有名字的時間點,之後可以被 measure 引用或在效能記錄裡看到。

mark 是「點」,本身沒有長度。名字是自由字串,也是你唯一的識別方式,所以慣例上會用 xxx-start / xxx-end 成對命名。它幾乎沒有成本,但會累積在 buffer 裡(預設上限數百筆),長時間執行的頁面應該定期 clearMarks()。

相關術語: performance.measure() (被它引用)

出處:第 4 段「mark / measure:把一個操作拆成有名字的步驟」

performance.measure()

命名區段

把兩個 mark 之間的區間算成一筆有名字、有 duration 的測量結果。

它是 mark 的成果收割:給 measure 一個名字與起訖 mark,就得到一筆 PerformanceMeasure,帶 name、startTime、duration。相較於自己算兩個 now() 的差,好處是結果有名字、會進 timeline、且能被 PerformanceObserver 即時監聽。新版 API 也允許直接傳時間數字而不必先建 mark。

相關術語: performance.mark() (以它為端點)、performance.getEntriesByType() (由它取回)

出處:第 4 段「mark / measure:把一個操作拆成有名字的步驟」

performance.getEntriesByType()

依類型取回測量結果

從瀏覽器的 performance timeline 撈出某一類的紀錄,例如所有 measure、resource 或 navigation。

它是同步的一次性查詢,回傳當下 buffer 裡的所有紀錄,所以會撈到之前累積的舊資料。若要「一有新結果就處理」,用 PerformanceObserver 比反覆輪詢這個函式好,也不會因為 buffer 被塞爆而漏掉紀錄。

相關術語: performance.measure() (取回其結果)

出處:第 4 段「mark / measure:把一個操作拆成有名字的步驟」

console.table()

表格化輸出

把陣列或物件在 DevTools console 印成一張欄列分明的表,而不是一行行文字。

它跟效能無關,純粹是可讀性工具,但在這裡很關鍵:三筆命名測量並排成表,比例的傾斜一眼就看得到,而三行 console.log 不會。傳入的每個元素會變成一列,物件的 key 變成欄;第二個參數可以指定只顯示哪些欄。

相關術語: performance.getEntriesByType() (顯示其結果)

出處:第 4 段「mark / measure:把一個操作拆成有名字的步驟」

留給下一段 現在知道慢的是 search 這一步了,但手上仍然只有「哪一步、多久」——那 8 毫秒(換成真實資料量就是幾百毫秒)究竟是花在 JavaScript 計算、layout 還是畫面繪製,完全看不出來。

5. Performance API 的天花板:知道多久,不知道為何 5:44–6:11

作者點出一個重要限制:Performance API 只告訴我們某件事花了多久,不告訴我們為什麼。搜尋花兩百毫秒是有用的資訊,但那是 JavaScript?是 React?是 layout 還是 rendering?這個 API 回答不了。要回答,就需要 profiler。

承上 上一段留下的正是「知道哪一步慢,卻不知道那段時間花在什麼上」。作者這一段把它正式講成 Performance API 的限制,不再當成使用技巧的問題。

推理因為上一段已經把操作拆到步驟層級、卻仍然只得到「多久」,所以作者接著把問題本身換掉:從 how long 換成 why。他用一個假設數字示範這個換位——搜尋花了兩百毫秒是有用的資訊,但那是 JavaScript?是 React?是 layout 還是 rendering?——並直接宣告這個 API 回答不了,要回答就需要 profiler。

AI 補充根本原因在於量測的視角不同:mark / measure 量的是「你的程式碼從哪一行到哪一行」,它對瀏覽器在同一段時間裡做的樣式計算、版面配置、繪製一無所知。同一個兩百毫秒,可能是一個 JS 迴圈,也可能是你改了一個 class 觸發整頁 reflow,在 measure 眼裡長得一模一樣。要區分,需要的是一個站在瀏覽器那一側、能列出每個 task 與呼叫堆疊的工具,也就是 profiler。不過作者這裡的斷言在今天已經沒有那麼絕對(見本段勘誤):PerformanceObserver 搭配 long-animation-frame、layout-shift、event 等 entry type,其實能給出部分歸因,只是給不出 call stack——「知道 layout 佔了多久」和「知道是哪一行程式碼觸發的」仍是兩件事。

術語:Long Animation Frames API

profiler

效能剖析器

在程式執行期間持續取樣或記錄,事後告訴你時間分別花在哪些函式、哪些工作上的工具。

碼錶式測量要你事先知道「要量哪一段」,profiler 反過來:先錄下來,再從結果裡找出重點。代價是錄製本身有成本、資料量大、需要解讀技巧。前端常見的 profiler 有兩層:瀏覽器層(Chrome Performance 面板)與框架層(React Profiler),它們看到的東西不同,不能互相取代。

相關術語: Performance API (補其不足)

出處:第 5 段「Performance API 的天花板:知道多久,不知道為何」

layout

版面配置(reflow)

瀏覽器根據樣式計算每個元素幾何位置與大小的階段,也叫 reflow。

layout 屬於瀏覽器內部工作,不出現在你的呼叫堆疊裡,所以自己夾 performance.now() 量不到——除非你在讀取 offsetHeight 這類會強制同步 layout 的屬性,那時成本會突然算到你的程式碼頭上(俗稱 layout thrashing)。它和 rendering/painting 是渲染管線上不同的階段,混為一談會找錯優化方向。

相關術語: profiler (才看得到)

出處:第 5 段「Performance API 的天花板:知道多久,不知道為何」

Long Animation Frames API

長動畫影格 API

較新的 PerformanceObserver entry type,會回報造成畫面卡頓的長影格,並附上腳本來源與樣式/版面耗時。

它是 Long Tasks API 的後繼者:Long Tasks 只告訴你「有一個超過 50ms 的 task」,LoAF 則把整個影格拆開,給出 scripts 陣列(含來源 URL、function 名)與 styleAndLayoutDuration。這讓「Performance API 完全無法回答 why」這個說法在今天變得比較寬鬆——但它仍然是聚合統計,不是逐格的呼叫堆疊,所以定位到單一函式還是要靠 profiler。它主要用於線上真實使用者監控(RUM),開發時仍以 DevTools 為主。

相關術語: Performance API (屬於其一部分)

出處:第 5 段「Performance API 的天花板:知道多久,不知道為何」

見仁見智 5:58
「Is it layout or is it rendering? The performance API cannot answer these questions.」
作為「why 要靠 profiler」的教學對比,這句話成立;但講成 Performance API 完全回答不了 layout / rendering 的歸因,以今天的瀏覽器來說偏絕對。PerformanceObserver 的 long-animation-frame entry 會給出該影格的 styleAndLayoutDuration 與造成阻塞的腳本來源,layout-shift、event、element 等 entry type 也各自提供部分歸因。準確的說法是:Performance API 給得出「哪一類工作佔了多久」,但給不出呼叫堆疊,所以要定位到某一行程式碼仍然需要 profiler。
依據: Long Animation Frames API(Chrome 123 起預設開啟,2024);PerformanceObserver 支援的 layout-shift / event / element entry type(Event Timing、Layout Instability 規格)。
留給下一段 剩下的問題變成:有沒有一個工具,能把整段互動期間瀏覽器做的每一件事都錄下來,而不是只量我親手夾住的那一段程式碼?

6. Chrome Performance:錄下整段互動 6:11–7:04

假設已知 log 分析約四百毫秒,但不知道這四百毫秒去哪了。Chrome performance 面板記錄的不是單一函式,而是整段互動期間瀏覽器在做什麼。操作方式:打開 DevTools 的 Performance 面板,按角落的 record 按鈕(或快捷鍵),執行同一個搜尋(搜 discount),拿到結果後停止錄製,Chrome 會產生一份報告。

DevTools Performance 面板與 record 按鈕的位置,照著點才錄得起來。
6:42 · DevTools Performance 面板與 record 按鈕的位置,照著點才錄得起來。
承上 上一段留下的問題是「有沒有工具能把整段互動期間瀏覽器做的每件事錄下來」。Chrome DevTools 的 Performance 面板正是這個答案:它記錄的不是單一函式,而是整段期間瀏覽器在做什麼。

推理因為上一段判定需要一個 profiler,所以作者接著示範最容易取得的那一個——Chrome 內建的面板,不必安裝任何東西,延續了他一路以來「先用最便宜的工具」的取捨。關鍵的實驗設計是:他刻意重跑同一個搜尋(搜 discount),讓新工具量的和先前 baseline 量的是同一件事,這樣兩邊的數字才對得起來。

AI 補充畫面上受測的是 localhost:5173 的 Log Analyzer(頁面標著 Ingested 50,000 logs),而且它本身就內建「Where analysis runs:Main thread / Web Worker」的切換與 LAST RUN 473.9 ms——這正好對得上作者口中「大約四百毫秒」的量級,也說明這個 demo 從一開始就是為了做前後對照而設計的。DevTools 這裡停在 Performance 面板的 Local metrics 首頁(LCP 0.07s、CLS 0.00、INP 24ms),錄製鈕在面板左上角,Cmd/Ctrl+E 是開始與停止的快捷鍵。有兩件作者略過但會直接影響結論的事:其一,錄之前應該把 CPU throttling 調成 4x 或 6x,否則在開發機上量到的 long task 會比真實使用者樂觀得多,畫面右下角的 Environment settings 就是設定的地方;其二,順序必須是「先按錄、再互動」,否則那次互動根本不在時間軸上。

術語:Chrome DevTools Performance panelInteraction to Next Paint (INP)

Chrome DevTools Performance panel

Chrome 效能面板

Chrome 內建的錄製式 profiler,記錄一段時間內瀏覽器各執行緒上的所有工作並以時間軸呈現。

它錄的不只是 JavaScript:網路、載入、動畫、互動、樣式計算、版面、繪製、GPU 都在同一條時間軸上,還會附截圖。因為資訊量大,正確用法是先想好「我要重現哪一次互動」再錄,而不是漫無目的錄一分鐘。新版 Chrome 還把 Local metrics(LCP/CLS/INP)與 Insights 併進同一個面板,一打開就先看到現場指標。

相關術語: profiler (一種)、CPU throttling (錄製前設定)

出處:第 6 段「Chrome Performance:錄下整段互動」

CPU throttling

CPU 降速模擬

DevTools 刻意把 CPU 放慢數倍,用來模擬中低階裝置上的執行速度。

開發機通常比使用者的手機快好幾倍,不降速就會系統性低估所有 JavaScript 成本,導致「在我機器上很快」的經典誤判。4x 大致對應中階手機、6x 更接近低階機。它只放慢 CPU,不影響網路,網路要另外用 network throttling。

相關術語: Chrome DevTools Performance panel (其設定項)

出處:第 6 段「Chrome Performance:錄下整段互動」

Interaction to Next Paint (INP)

互動到下次繪製

衡量頁面對使用者互動反應速度的指標:從互動發生到畫面實際更新之間的延遲。

它是 Core Web Vitals 之一,2024 年正式取代 FID。跟這支影片的主題直接相關:一個卡住主執行緒四百毫秒的分析函式,最先毀掉的就是 INP。畫面上顯示 24ms(綠色)是因為當時只按了鍵盤、還沒觸發那個重運算。一般以 200ms 為良好門檻。

相關術語: main thread (被其阻塞)

出處:第 6 段「Chrome Performance:錄下整段互動」

留給下一段 錄是錄下來了,但一份十幾秒、疊了好幾條軌的時間軸本身也還不是答案——得從裡面把那四百毫秒定位到某一條軌、某一個函式上。

7. 火焰圖指出:時間全花在 main thread 7:04–8:29

作者不重講每個面板,只聚焦一個想法:Performance API 答的是「花了多久」,Chrome performance 答的是「瀏覽器把時間花在哪」。這份錄製裡 main thread 上立刻看得到一個 long task,放大後大部分時間落在 simulateHeavyWork。真正的問題不是互動花了四百毫秒,而是這四百毫秒幾乎都在 main thread 跑 JavaScript。到這裡才有依據決定要改演算法、搬進 web worker、還是根本別做這件事——不再是猜測,而是根據測量做決定。

錄製完成後的完整 timeline 全貌,是「瀏覽器把時間花在哪」的原始素材。
7:08 · 錄製完成後的完整 timeline 全貌,是「瀏覽器把時間花在哪」的原始素材。
放大後的 long task 與 simulateHeavyWork:這一格火焰圖就是本段結論的證據。
7:40 · 放大後的 long task 與 simulateHeavyWork:這一格火焰圖就是本段結論的證據。
承上 上一段結束在「錄好了,但還沒從時間軸裡定位出那四百毫秒」。這一段作者刻意不重講每個面板,只用一個問題把整份錄製收斂:瀏覽器把時間花在哪裡。

推理因為上一段拿到的是一份資訊量過大的時間軸,所以作者接著做的是「限縮問題」而不是「全面導覽」:他把兩個工具各自回答的問題並排——Performance API 答「花了多久」、Chrome Performance 答「時間花在哪」——順著這個問題一眼找到 main thread 上的 long task,放大後看見大部分時間落在 simulateHeavyWork。由此他改寫了問題的敘述:真正的麻煩不是互動花了四百毫秒,而是這四百毫秒幾乎都在 main thread 上跑 JavaScript。到這裡才有依據談改演算法、搬 web worker 或乾脆不做。

AI 補充兩張圖分別對應兩個判讀層次。全景那張的左下角 Summary 已經給了答案的輪廓:13.53 秒的錄製裡 Scripting 3,373ms、Rendering 205ms、Painting 139ms、System 264ms——scripting 一項就壓倒其他所有,還沒放大就知道不必去查樣式或繪製。放大之後(範圍 2.73s–3.75s)在「Main — http://localhost:5173」軌上疊出完整呼叫堆疊:Function call → run → Run console task →(anonymous)→ runMainThreadDemoAnalysis → runDemoAnalysis → simulateHeavyWork,tooltip 寫著 209.49 ms(self 193.36 ms),來源 src/lib/analysis.ts。這裡有一個作者沒講、但是「該不該改演算法」的判斷關鍵:self time 幾乎等於 total time,代表時間真的燒在這個函式自己的迴圈裡,而不是它呼叫的別人——如果 self 很小、total 很大,該優化的就是被它呼叫的下層而不是它。另外 Frames 軌上那格 39.5ms 說明畫面在這段期間確實掉了幀,這是使用者真正感覺到的東西。順帶一提,畫面上的 long task 是兩百毫秒級,而作者口中的四百毫秒是整次互動(含多次分析)的量級,兩個數字尺度不同,不必硬對在一起。

術語:flame chart

main thread

主執行緒

瀏覽器用來跑 JavaScript、樣式計算、版面配置與大部分渲染工作的單一執行緒;它被佔住,畫面就不會更新。

「單一」是重點:JavaScript 執行和畫面更新共用同一條執行緒,所以一個跑很久的同步函式會直接讓頁面停止回應——不是變慢,是完全不動。這解釋了為什麼把運算搬進背景執行緒(見第 1 段)是常見解法:不是讓運算變快,而是把主執行緒空出來。

相關術語: long task (發生於)、web worker (用以卸載)

出處:第 7 段「火焰圖指出:時間全花在 main thread」

long task

長任務

在主執行緒上連續執行超過 50 毫秒的工作,期間瀏覽器無法回應使用者互動。

50 毫秒這個門檻來自 RAIL 模型:使用者互動要在 100 毫秒內有反應,扣掉其他開銷後留給單一 task 的預算大約就是 50 毫秒。DevTools 會在時間軸上把超標的 task 標紅角。解法通常是切碎(yield 回瀏覽器)、搬走(背景執行緒)或根本不做,而不是把同一段程式碼寫得快一點點。

相關術語: main thread (阻塞它)

出處:第 7 段「火焰圖指出:時間全花在 main thread」

flame chart

火焰圖

把呼叫堆疊畫成上下堆疊的橫條,橫軸是時間、縱軸是呼叫深度,愈往下代表被誰呼叫。

讀法只有兩條:找最寬的橫條(花最多時間),然後往下看它的最底層(真正在做事的人)。注意 DevTools 的火焰圖是時間順序的(同一函式被呼叫多次會出現多次),跟把相同函式合併統計的「火焰圖(aggregated flame graph)」不是同一種圖,後者在 Bottom-up / Call tree 分頁才看得到。

相關術語: self time (由其寬度讀出)

出處:第 7 段「火焰圖指出:時間全花在 main thread」

self time

自身耗時

一個函式扣掉它所呼叫的子函式之後,自己真正花掉的時間。

它和 total time 的差距決定了該往哪裡優化:self 接近 total,代表瓶頸就在這個函式的迴圈或計算本身;self 很小而 total 很大,代表它只是轉包,該看的是下層。DevTools 的 tooltip 會同時顯示兩者(例如 209.49 ms 中 self 193.36 ms),Bottom-up 分頁則直接以 self time 排序,是找熱點最快的一頁。

相關術語: flame chart (從中判讀)

出處:第 7 段「火焰圖指出:時間全花在 main thread」

留給下一段 main thread 上的 JavaScript 被指認成主嫌了,但同一張截圖左側還有 Scheduler 與 Components 兩條 React 專屬的軌沒有讀——React 自己渲染那些結果花了多少時間,仍是一片空白,也就無法排除「是 React 渲染太多元件」這個可能。

8. React Profiler:只量你在意的那棵子樹 8:29–9:52

前面把瀏覽器當整體看,但若想知道 React 本身在做什麼呢?也許昂貴的不是演算法,而是 React 渲染了遠比預期多的元件。React 提供 Profiler 元件:作者通常不 profile 整個 app,只把在意的部分(這裡是搜尋結果)包起來,並給一個 onRender callback。每次這棵子樹渲染完,React 就呼叫它。callback 給的值很多,他只看三個——id(在量哪一塊)、phase(初次渲染還是更新)、以及最常看的 actualDuration(React 花多少時間渲染這棵子樹)。示範中結果是約 30 毫秒。

用 Profiler 元件包住目標子樹並掛上 onRender 的寫法,是這一段唯一需要照抄的程式碼。
9:08 · 用 Profiler 元件包住目標子樹並掛上 onRender 的寫法,是這一段唯一需要照抄的程式碼。
onRender callback 的參數列表,作者只挑三個看——畫面上其他參數的存在感解釋了他為何要挑。
9:35 · onRender callback 的參數列表,作者只挑三個看——畫面上其他參數的存在感解釋了他為何要挑。
actualDuration 約 30 毫秒的實際輸出,下一段用它跟四百毫秒做對照。
9:49 · actualDuration 約 30 毫秒的實際輸出,下一段用它跟四百毫秒做對照。
承上 上一段留下的空白是「React 自己渲染花了多少時間」。作者這一段就換上框架層的量尺:前面一直把瀏覽器當成整體看,現在要問的是 React 在做什麼——也許昂貴的不是演算法,而是 React 渲染了遠比預期多的元件。

推理因為上一段把嫌疑指向 main thread 上的 JavaScript、卻無法排除 React 渲染這條線,所以作者接著引入 React 內建的 Profiler 元件。他強調不 profile 整個 app,只把在意的那一塊(搜尋結果)包起來,並掛一個 onRender callback;每當這棵子樹渲染完,React 就呼叫它。callback 給的值不少,他只固定看三個:id(在量哪一塊)、phase(初次掛載還是更新)、以及最常看的 actualDuration(React 花多少時間渲染這棵子樹)。示範得到的數字約是 30 毫秒。

AI 補充程式碼截圖的註解把分工講得比旁白更清楚:Performance API 量的是 your logic,Profiler 量的是某一棵子樹的 React render / commit,「當瓶頸可能在 UI 而不是 analyzeLogs 時才用它」。寫法是 `<Profiler id='Results' onRender={onRender}><ResultsList logs={logs} /></Profiler>`,callback 簽章寫成 `onRender(id, phase, actualDuration, baseDuration)`。三點補充:其一,真實的 onRender 其實還有 startTime 與 commitTime 兩個參數,作者的範例只解構了前四個,需要把 React 的 commit 對到 Chrome 時間軸時就會用到後兩個。其二,畫面上出現、旁白卻沒提的 baseDuration 很值得看——它是「假設完全沒有 memo 化時的估計渲染時間」,和 actualDuration 一起看才知道 memo 到底有沒有省到;只看 actualDuration 很難回答「該不該加 memo」。其三,id 之所以是必填,是因為一個 app 可以放很多個 Profiler 而共用同一個 callback,沒有 id 就分不清這筆是誰。最後要留意,30 毫秒這個數字在影片裡是用字幕卡(React (30ms) Rendering)帶過去的,不是 console 印出來的實測畫面。

術語:React Profiler

React Profiler

React 剖析器元件

React 內建的 <Profiler> 元件,把一棵子樹包起來後,每次該子樹渲染完成就回報這次花了多少時間。

它是「框架層的碼錶」:量的不是你的商業邏輯,而是 React 自己 render 與 commit 這棵子樹的成本。和 React DevTools 的 Profiler 分頁是同一套機制的兩種介面——元件版適合把數字寫進 log 或送到監控,分頁版適合互動探索。因為它是元件,可以只包住可疑的區塊,避免整個 app 都付計時成本。

相關術語: onRender (透過它回報)、profiler (一種)

出處:第 8 段「React Profiler:只量你在意的那棵子樹」

onRender

渲染完成回呼

React 在被 Profiler 包住的子樹每次渲染提交後呼叫的函式,參數帶著這次渲染的識別與耗時。

完整簽章是 onRender(id, phase, actualDuration, baseDuration, startTime, commitTime)。它在 commit 階段之後被呼叫,所以拿到的是「已經發生」的事實,不能用來取消或延遲渲染。要注意 callback 本身如果做太多事(例如同步寫入或設定 state),會把觀測成本變成真的成本,甚至引發額外渲染。

相關術語: React Profiler (由它呼叫)、actualDuration (傳入參數)

出處:第 8 段「React Profiler:只量你在意的那棵子樹」

phase

渲染階段

這次渲染是初次掛載(mount)還是後續更新(update),也可能是 nested-update。

分開看很重要:mount 通常最貴(要建立整棵樹),拿它跟 update 比會得到誤導性的結論。使用者互動造成的卡頓幾乎都發生在 update,所以優化時要盯的是 update 的 actualDuration,而不是被一次性的 mount 數字嚇到。

相關術語: onRender (其參數)

出處:第 8 段「React Profiler:只量你在意的那棵子樹」

actualDuration

本次實際渲染耗時

React 這一次渲染這棵子樹實際花掉的毫秒數。

「實際」的意思是把 memo 化的效果算進去了:有被 memo 擋掉、沒有重新渲染的子元件不會計入。所以同一棵樹第一次和之後幾次的 actualDuration 常常差很多。它只涵蓋 React 的 render 與 commit,不包含你在事件處理器裡跑的運算——這正是它和 Performance API 的分界。

相關術語: baseDuration (對照組)

出處:第 8 段「React Profiler:只量你在意的那棵子樹」

baseDuration

未 memo 化的估計耗時

假設完全沒有任何 memo 化、整棵子樹都重新渲染時的估計時間。

它是 actualDuration 的對照組:兩者接近,代表你的 memo 幾乎沒發揮作用(或根本沒必要加);actualDuration 遠小於 baseDuration,代表 memo 化確實擋下了大量重繪。單看 actualDuration 很難判斷「加了 memo 有沒有用」,把這一對數字並排才回答得了。它是估計值而非量測值,用來比較趨勢,不要當精確數字。

相關術語: actualDuration (與之並排)

出處:第 8 段「React Profiler:只量你在意的那棵子樹」

留給下一段 手上多了 30 毫秒這個數字,但單獨看它沒有任何意義——它得跟先前量到的分析時間並排,才能回答「該不該去優化 React」。

9. 三個數字擺在一起才有結論 9:52–10:32

React 渲染 30 毫秒 vs 前面量到的 log 分析四百毫秒——對照之下立刻看出 React rendering 不是這裡最大的問題,時間還是花在 main thread 分析 log。這正是作者要把三個工具一起講的原因:只看 React profiler 會跑去優化元件;只看 Performance API 會知道慢但不知為何。合起來才有完整的圖像。

承上 上一段留下的是一個孤立的 30 毫秒,以及一句「它得跟先前的數字並排才有意義」。這一段作者就把它跟第 3 段建立的那條 baseline——log 分析本身約四百毫秒——擺在一起。

推理因為上一段拿到的數字單獨看不出好壞,所以作者接著做的其實只是一個除法:30 比 400 大約是 7%,於是結論立刻浮現——React rendering 不是這裡最大的問題,時間仍然花在 main thread 上分析 log。他順勢說明為什麼堅持要把三個工具一起講:只看 React Profiler 會跑去優化元件,只看 Performance API 會知道慢卻不知為何,合起來才有完整的圖像。

AI 補充這個對照能成立有一個沒被說出來的前提:兩個數字必須量的是同一次互動的同一段時間。如果那 30 毫秒來自另一次 render,或分析其實已經跑在背景執行緒上,兩者就不在同一條時間軸上,比例也就沒有意義——這也是為什麼作者前面要刻意重跑同一個搜尋。另外值得注意的是,工具的盲點是對稱的:若當初只看 React Profiler,看到 30 毫秒會滿意地說「React 很快,沒問題」,卻完全不知道使用者其實等了四百毫秒;反過來只看總時間,也可能冤枉了根本沒問題的 React。所謂「拼圖的一塊」不是修辭,而是說每個工具都有一整片自己看不到的區域。

術語:bottleneck

bottleneck

瓶頸

整個流程中佔掉最多時間、決定了整體速度的那一段;優化其他部分幾乎不會改變總時間。

這一段的除法就是在找瓶頸:400 毫秒裡 React 只佔 30,就算把 React 渲染優化到零,使用者的感受也幾乎不變。這是 Amdahl 定律的日常版本——可優化幅度的上限,等於那一段佔總時間的比例。先量再優化的真正理由就在這裡:找錯瓶頸的努力,報酬率趨近於零。

相關術語: baseline (靠它定位)

出處:第 9 段「三個數字擺在一起才有結論」

留給下一段 結論看起來乾淨俐落,但它整個建立在這些數字可信的前提上;而測量本身會不會影響被測對象、開發模式下的數字能不能當真,到這裡都還沒有交代。

10. Profiler 的數字怎麼讀才對 10:32–11:20

作者補充一個前提:profiling 本身有 overhead,所以 React Profiler 在正式的 production build 是關掉的;development mode 的數字也偏吵,尤其開了 StrictMode。因此他把這些數字當作「A 實作對 B 實作」的相對比較用,而不是拿來當 production 的絕對值。他也提到 React 可以整合進 Chrome performance,在 timeline 上看到 React 專屬的時間資訊,那值得另開一支影片。

承上 上一段結尾留下「這些數字到底能不能當真」。作者這一段就補上使用前提:profiling 本身有 overhead,所以 React Profiler 在正式的 production build 是關掉的;development mode 的數字也偏吵,開了 StrictMode 更明顯。

推理因為上一段的結論完全建立在數字的可信度上,所以作者接著不是繼續往下推,而是回頭替自己的結論設界線:他把這些數字定位成「A 實作對 B 實作的相對比較」,而不是可以拿去對外宣稱的 production 絕對值。這個限縮反而讓前一段的結論更站得住——30 比 400 這種數量級的差距,不會因為量測誤差而翻盤。

AI 補充StrictMode 為什麼吵值得說清楚:它在開發模式下會刻意把 render 函式跑兩次,用來逼出不純的副作用,因此 actualDuration 會系統性偏高,而且不是穩定的兩倍(第二次通常較快,快取已熱),所以連「除以二」都不可靠。production build 之所以關掉 Profiler,是因為每個 commit 都要記時間戳與累加,那個成本不該讓所有使用者付;真要在接近正式的環境測,得改用特別開啟 profiling 的 build。至於作者提到「React 也可以整合進 Chrome performance」——這件事其實在他自己的錄製畫面上就已經發生了:第 7 段那兩張時間軸截圖左側的 `Scheduler ⚛` 與 `Components ⚛` 兩條軌,就是 React 寫進瀏覽器時間軸的自訂軌道,它讓你在同一條時間軸上同時看到 React 的工作與底下的 JavaScript 呼叫堆疊,某種程度上就是把這一段和前面兩個工具接起來。

術語:profiling overhead

profiling overhead

剖析開銷

量測動作本身消耗的時間,會讓被測程式看起來比實際更慢。

這是觀測者效應的工程版本:計時、記錄堆疊、寫入緩衝都要成本,取樣愈密成本愈高。實務上的處理方式不是消除它,而是承認它並且讓它「等量地」出現在被比較的兩邊——A 實作和 B 實作都帶著同樣的 overhead,差值仍然有意義。這也是作者把數字當相對比較用的理由。

相關術語: production build (因此被關閉)

出處:第 10 段「Profiler 的數字怎麼讀才對」

production build

正式版建置

拿掉開發期檢查與警告、經過最佳化與壓縮、實際部署給使用者的那份程式碼。

React 的 production build 會移除開發用的警告、額外檢查與 profiling 記帳,所以速度和 development 差距可能到數倍——這也是為什麼拿 dev 模式的數字去嚇自己沒有意義。若真的需要在接近正式的條件下 profile,React 提供了額外開啟 profiling 的建置設定,代價是少量效能。

相關術語: profiling overhead (為省它而關)

出處:第 10 段「Profiler 的數字怎麼讀才對」

StrictMode

嚴格模式

React 的開發用包裝元件,會刻意重複執行 render 與 effect,用來提早暴露不安全的副作用。

它只在開發模式生效,production 完全不會雙跑,所以它造成的數字膨脹是「假的慢」。重點是它不是為了效能而存在,而是為了正確性:雙跑能抓出把副作用寫進 render、或 effect 沒寫清理函式這類問題。profile 時如果覺得數字怪高,先確認是不是被它影響,而不是急著去優化元件。

相關術語: actualDuration (使其偏高)

出處:第 10 段「Profiler 的數字怎麼讀才對」

見仁見智 10:58
「React can also integrate with Chrome performance where you will see」
這句在影片脈絡裡被講成一個通則,但它其實高度依賴版本。React 早年靠 User Timing marks 提供的 DevTools 時間軸資訊在 React 18 已被移除;現在畫面上看得到的 `Scheduler ⚛` 與 `Components ⚛` 自訂軌道,是 React 19.1 之後才加入的 Performance tracks 功能,而且只在開發模式與支援自訂軌的 Chrome 版本才會出現。用舊版 React 或 production build 的人照著做會看不到任何 React 軌道。
依據: React 19.1 release notes(2025)新增 Performance Tracks(Scheduler ⚛ / Components ⚛);React 18 移除 unstable_trace 與 User Timing 整合;Chrome DevTools extensibility API for custom tracks(Chrome 125+)。
留給下一段 三個工具、三種數字、以及讀數字的規矩都齊了,剩下的是把它們收成一條可以照著做的行動守則。

11. 總結:先量,再決定要優化什麼 11:20–13:12

快速複習:三個工具各答一個問題——Performance API 是操作花多久、Chrome performance 是瀏覽器把時間花在哪、React Profiler 是一次 React render 有多貴,誰也取代不了誰,各給拼圖的一塊。所以下次有人說「頁面感覺很慢」,不要直接跳進優化:先量、建立 baseline、搞清楚時間去哪,然後才決定改什麼。作者最後點出這支影片只是效能的「測量」這一半,找到瓶頸之後的取捨(web worker、快取、渲染策略、資料流、虛擬化、code splitting)是他課程與書的範圍,React Profiler 也值得單獨一支深入影片。

recap 時三個工具與各自回答的問題並列在畫面上,是整支影片最適合截下來當備忘的一張。
11:30 · recap 時三個工具與各自回答的問題並列在畫面上,是整支影片最適合截下來當備忘的一張。
承上 上一段給出「數字當相對比較用」的守則後,三個工具與它們的使用前提都齊了。這一段作者把它們並排收束成一句話:每個工具回答的問題都不一樣,誰也取代不了誰。

推理因為前面三個工具是被各自的不足一個接一個逼出來的,所以作者最後把三個問題排在一起做結——Performance API 是「這個操作花多久」、Chrome performance 是「瀏覽器把時間花在哪」、React Profiler 是「一次 React render 有多貴」——再回到開場那句「頁面感覺很慢」,給出行動守則:不要直接跳進優化,先量、建立 baseline、搞清楚時間去哪,然後才決定改什麼。最後他明確劃界:這支影片只做完測量這一半,找到瓶頸之後的取捨屬於他的課程與書。

AI 補充把三個工具看成三種「解析度」會比背誦三句話更好用:Performance API 是你自己插的探針,解析度等於你打點的粒度;Chrome Performance 是瀏覽器的全景錄影,解析度是每個 task 與呼叫堆疊;React Profiler 是框架層的一次 commit,解析度是一棵子樹一次渲染。使用順序就是解析度由粗到細——先用最便宜的確認「真的有問題、大概多大」,再花力氣錄時間軸找位置,最後才進到框架內部。畫面上的字卡是三個工具逐一出現的,這一張停在 Chrome Performance/Where the browser spends its time。另外值得留意作者劃界的方式:他點名的下一半題目——web worker(見第 1 段)、caching、rendering strategy、virtualization、code splitting(見第 1 段)——每一個都改變了系統結構,因此每一個都必須先有 baseline 才知道是賺是賠,正好把整支影片繞回開場的那個迴圈。

virtualization

虛擬清單

長列表只渲染畫面上看得見的那幾列,捲動時才動態替換,避免一次建立上萬個 DOM 節點。

它針對的是「渲染太多元件」這一類瓶頸,也就是 React Profiler 最容易照出來的那種。代價不小:捲動位置、可變高度、鍵盤操作與搜尋(Ctrl+F 找不到未渲染的內容)都要另外處理,所以更需要先量過確定值得。

相關術語: React Profiler (由它照出)

出處:第 11 段「總結:先量,再決定要優化什麼」

rendering strategy

渲染策略

決定畫面在哪裡、什麼時候被產生出來:客戶端渲染、伺服器渲染、預先產生或串流。

它是比「優化某個函式」高一層的決定:同一份資料,改成在伺服器算好再送,客戶端的 main thread 成本可能整段消失。因為影響的是整體架構,錯誤的選擇很難靠微優化補救,這也是作者把它留給課程的原因。

相關術語: performance optimization (上層決策)

出處:第 11 段「總結:先量,再決定要優化什麼」

留給下一段 總結收束。

4. 總結

作者先拆解「這頁很慢」這句話:有多慢、哪裡慢、跟什麼比——沒有這三個答案,優化就只是猜測,所以要先有 baseline。第一把尺是 Performance API,因為它內建於瀏覽器、不綁框架,便宜到可以隨時重跑;用 performance.now() 夾住 analyzeLogs 就得到第一個總數。但總數看不出該改哪一步,於是換成 mark / measure 把 search、filter、aggregate 分別命名量測,用 console.table 列成一張表。表格指出 search 最慢,卻答不出那段時間是花在 JavaScript、layout 還是繪製——這是 Performance API 的天花板,也逼出第二把尺。Chrome Performance 錄的不是單一函式,而是整段互動期間瀏覽器做的每件事;火焰圖上 main thread 的 long task 把四百毫秒定位到 simulateHeavyWork,結論從「很慢」變成「幾乎全部花在 main thread 跑 JS」。但瀏覽器層的視角看不進框架內部,於是第三把尺 React Profiler 上場:包住在意的子樹、掛 onRender,只看 id、phase、actualDuration 三個值,量到 React 渲染約 30 毫秒。關鍵在於把 30 毫秒與先前的四百毫秒並排——React 不是瓶頸,直覺被數據推翻。最後作者補上讀數字的規矩(profiling 有 overhead、production build 關閉、StrictMode 會吵,所以只當相對比較用),收束成一句:三個工具各答一個問題,誰也取代不了誰;下次聽到「頁面很慢」,先量、建 baseline、看時間去哪,然後才決定改什麼。

勘誤總整理

確定錯誤/已過時見仁見智(取決於版本或情境)

段落原話(transcript 逐字)說明
5. Performance API 的天花板:知道多久,不知道為何
5:58
「Is it layout or is it rendering? The performance API cannot answer these questions.」作為「why 要靠 profiler」的教學對比,這句話成立;但講成 Performance API 完全回答不了 layout / rendering 的歸因,以今天的瀏覽器來說偏絕對。PerformanceObserver 的 long-animation-frame entry 會給出該影格的 styleAndLayoutDuration 與造成阻塞的腳本來源,layout-shift、event、element 等 entry type 也各自提供部分歸因。準確的說法是:Performance API 給得出「哪一類工作佔了多久」,但給不出呼叫堆疊,所以要定位到某一行程式碼仍然需要 profiler。
依據: Long Animation Frames API(Chrome 123 起預設開啟,2024);PerformanceObserver 支援的 layout-shift / event / element entry type(Event Timing、Layout Instability 規格)。
10. Profiler 的數字怎麼讀才對
10:58
「React can also integrate with Chrome performance where you will see」這句在影片脈絡裡被講成一個通則,但它其實高度依賴版本。React 早年靠 User Timing marks 提供的 DevTools 時間軸資訊在 React 18 已被移除;現在畫面上看得到的 `Scheduler ⚛` 與 `Components ⚛` 自訂軌道,是 React 19.1 之後才加入的 Performance tracks 功能,而且只在開發模式與支援自訂軌的 Chrome 版本才會出現。用舊版 React 或 production build 的人照著做會看不到任何 React 軌道。
依據: React 19.1 release notes(2025)新增 Performance Tracks(Scheduler ⚛ / Components ⚛);React 18 移除 unstable_trace 與 User Timing 整合;Chrome DevTools extensibility API for custom tracks(Chrome 125+)。

5. 推薦三個下一步

1. 往下挖深:把火焰圖讀懂

第 7 段作者刻意跳過面板細節,只用「時間花在哪」收斂整份錄製。但 self time 與 total time 的差別、Scheduler / Components 這些 React 專屬軌怎麼讀,是把 Chrome Performance 從「看得出誰最寬」升級成「說得出為什麼」的關鍵缺口。

YouTube 搜尋:chrome devtools performance flame chart explained self time vs total time devtools chrome performance panel deep dive 2025

2. 往旁邊對照:實驗室數據 vs 真實使用者數據

全片量的都是自己機器上的一次操作(lab data),但第 1 段那句「跟什麼比」在生產環境的答案是真實使用者的分佈。Core Web Vitals 與 INP、以及 PerformanceObserver 蒐集 field data,正好補上這個對照面。

YouTube 搜尋:Core Web Vitals INP explained PerformanceObserver real user monitoring lab data vs field data web performance

3. 往上應用:量到瓶頸之後怎麼修

作者自己說這支只講了測量這一半,找到瓶頸後的取捨(web worker、快取、虛擬化、渲染策略)留給了課程。第 7 段那個卡住 main thread 的 long task,正是 web worker 與時間切片最典型的適用場景。

YouTube 搜尋:web worker offload main thread javascript long task breaking up main thread work react list virtualization performance

📄 全部影片 · 主題區: 前端底層與系統設計