瀏覽器其實是一支五人隊(下):一次畫面更新,就是一波 16 毫秒的團戰
上集認完了瀏覽器這支五人隊——隊長、打野、守線英雄、輸出核心、工具人。每個分頁派一個守線英雄,被網站隔離跟沙箱關得死死的,所以一個分頁崩了不會帶走全家。
這集講他們怎麼打一場團戰。
因為「把畫面顯示出來」這件事,不是一次就結束——只要滑頁面、有動畫、有東西在動,這支隊伍就得一秒鐘重打 60 次團戰。而且每一波,他們只有大約 16 毫秒可以打完整套連招。打完了,畫面順順的;打不完,就是那個卡卡的、一格一格的「掉幀」。
這 16 毫秒怎麼來的?很簡單的算術:螢幕一秒要更新 60 次(60 幀),1000 毫秒 ÷ 60 ≈ 16.7 毫秒一幀。在這個時間視窗內,這支隊伍要從一坨 HTML 字串,一路打到螢幕上的像素。
(還沒讀上集的話,先去認人:瀏覽器其實是一支五人隊(上)。這篇預設讀者已經知道守線英雄是誰。)
🏰 第 0 層:全景圖 — 一套連招,每秒打 60 次
守線英雄要把畫面打出來,這套連招的順序是固定的,一步接一步:
瀏覽器的繪製流程:DOM → 樣式 → 排版 → 上色 → 合成
關鍵在於:這套連招的前半段(解析、樣式、排版、上色)是守線英雄自己一個人(主執行緒)扛的,而後半段(合成)有副手(合成執行緒)跟輸出核心幫忙。這個分工是整篇的重點——它決定了為什麼有些畫面更新很順、有些卻會卡。
Mogu 偷偷說:
很多人以為「瀏覽器把網頁畫出來」是一個動作,按一下就好。其實它是一套每秒重複 60 次的連招。
你現在滑這篇文章,每往下滑一點,這套連招就在背景跑一遍。你覺得很順,是因為這支隊伍每次都在 16 毫秒內打完。哪天你滑某個網頁覺得「怎麼卡卡的」,不是你手機爛,是那支隊伍有人拖到後腿、連招接不完——下面會抓出兇手是誰 (¬‿¬)
🏰 第 1 層:連招前搖 — 守線英雄一個人扛的活
連招的前半段,全部壓在守線英雄的主執行緒(main thread)這一個人身上。它要做三件事:
- 解析 HTML → DOM:把一串文字標籤變成一棵樹狀結構(DOM),代表頁面的骨架。這個解析是邊下載邊做的,HTML 還沒下載完就能先開始建。
- 解析 CSS →
CSSOM:把樣式表變成另一棵樹,代表所有樣式規則。 - 算最終樣式:把 DOM 跟
CSSOM合起來,算出「每個元素最後到底長什麼樣」(什麼顏色、多大、什麼字體)。
但這裡有個會打斷前搖的陷阱:<script> 會卡住解析。瀏覽器解析 HTML 解到一半,撞到一個沒加 defer/async 的 <script>,會停下來先把這段 JS 跑完,才繼續解析。因為這段 JS 可能會改 DOM,瀏覽器不敢往下走。
Mogu 畫重點:
「為什麼我網頁第一秒是白的?」十之八九就是這個——某個會擋住畫面的
<script>卡在前面,守線英雄被迫先去跑 JS,畫面只好乾等。加一個
defer屬性,常常就是「白畫面三秒」變「白畫面零秒」的差別。一個字的修改,效能差一個數量級。前端的世界就是這麼樸實無華又樸實無華 ( ̄▽ ̄)
🏰 第 2 層:排兵布陣 + 上色 — 還是主執行緒的活
前搖比完,守線英雄知道「場上有什麼、每個東西長怎樣」了。接下來還是它一個人的活:
排版(layout,又叫 reflow):算出每一個元素的方框確切站在哪(x, y 座標)、多大(寬、高)。聽起來簡單,其實超複雜——要處理文繞圖、彈性盒、格線、行高、邊界重疊一堆規則。瀏覽器引擎有整個團隊花好幾年就在搞這一步。
這一步很貴。而且關鍵是:它會被牽連重算。如果等下有 JS 改了某個元素的大小、或塞了新內容進去,瀏覽器可能得把一部分(最慘是整頁)的排版重算一遍。這就是「排版很貴,少碰為妙」這句效能金句的由來。
上色(paint):排版算完位置,接著守線英雄走一遍版面樹,產生一份「畫圖指令清單」(顯示列表,display list)——「在這個座標畫一個藍色方塊、然後在那裡畫一行字、再疊一張圖」,照 z-index 順序排好,疊放關係才不會錯。
注意:到這裡還沒有任何一個像素被畫到螢幕上。守線英雄只是把「要畫什麼」這份清單列好了。真正的下筆,是下一層的事。
Mogu 真心話:
有沒有發現,到目前為止累死累活的都是守線英雄一個人(主執行緒):解析、算樣式、排版、列上色清單,全是它扛。
這就是為什麼前端優化老是在講「別讓主執行緒太忙」。它是隊上最忙的人,你又丟一堆肥 JS 給它跑,它就排不出時間比連招、畫面就卡。把工作從主執行緒搬走(搬給副手或 GPU),是效能優化的核心思路。記住這個人,下一層就要看誰能幫它分擔 ʕ•ᴥ•ʔ
到「上色」這一步結束,螢幕上畫好像素了嗎?
『上色(`paint`)』產生的是顯示列表——一份照順序排好的畫圖指令,像畫家先規劃『先畫背景、再畫文字』,這時還沒有半個像素落地(所以 B 錯)。真正把指令變成像素,是後面的『點陣化 + 合成』由副手跟 GPU 接手;畫面也不是一半一半生出來的(C 錯),而是整幀一起合成。
正確答案是 A
『上色(`paint`)』產生的是顯示列表——一份照順序排好的畫圖指令,像畫家先規劃『先畫背景、再畫文字』,這時還沒有半個像素落地(所以 B 錯)。真正把指令變成像素,是後面的『點陣化 + 合成』由副手跟 GPU 接手;畫面也不是一半一半生出來的(C 錯),而是整幀一起合成。
🏰 第 3 層:收頭 — 副手登場,為什麼滑動可以很順
清單列好,前半段結束。連招的收頭,守線英雄不再一個人扛了——它把活交給同一個進程裡的另一雙手:合成執行緒(compositor thread)。
這是整篇最重要的一個設計:合成執行緒跟主執行緒是分開的兩條線。 也就是說,就算主執行緒正忙著跑一坨肥 JS、卡得要死,合成執行緒還是能繼續動。
它做這幾件事:
- 分圖層:把頁面拆成幾個可以獨立處理的圖層(
layer)。會動的、有transform的、影片、canvas通常各自一層。 - 點陣化:把每個圖層切成一塊塊小圖磚(
tile,常見 256×256 或 512×512),丟給一票點陣化小工(raster worker)平行處理,用繪圖函式庫Skia把「畫圖指令」真的變成像素點,存進 GPU 記憶體。 - 合成出一幀:把所有圖磚組成一個「合成幀」,透過跨進程通訊送給隊長,最後由輸出核心(GPU 進程)用顯卡把這些圖層疊好、貼到螢幕上。
Mogu 內心戲:
這個「主執行緒卡死、滑動還是很順」的現象,第一次知道的人都會愣一下:蛤,畫面在動不就代表瀏覽器沒當嗎?
不一定。滑動順是合成執行緒的功勞,它跟跑你 JS 的主執行緒是兩個人。所以你可能滑得很爽,但一點按鈕發現「咦怎麼沒反應」——因為按鈕的事件要主執行緒處理,而它正埋在你那段
for迴圈裡出不來。畫面順 ≠ 網頁活著,這是兩條命 (⌐■_■)
🏰 第 4 層:16 毫秒的生死 — 為什麼有的動畫順、有的卡
現在懂了「主執行緒 vs 合成執行緒」這條分界線,就能解開前端最常見的效能謎題:為什麼同樣是做動畫,有的滑順如絲、有的卡成幻燈片?
答案就看:這個動畫需不需要驚動忙碌的主執行緒。
順的動畫(只走合成執行緒):只改 transform(位移、縮放、旋轉)或 opacity(透明度)。這兩個性質不影響排版、不影響上色——合成執行緒手上已經有那個圖層的像素了,它只要把圖層挪個位置、改個透明度,就能生出新的一幀。主執行緒就算在忙,這種動畫照樣 60 幀順順跑。
卡的動畫(綁死主執行緒):去動 height、top、width 或 background-color 這種會改變幾何或顏色的性質。慘了——每一幀都得回去重跑排版或上色,而那是主執行緒一個人的活。主執行緒只要稍微忙一點,這套連招就在 16 毫秒內打不完,畫面就掉幀。
還有一個經典自爆叫 強制同步重排(layout thrash):在 JS 裡剛改完某個元素的樣式,下一行馬上又去讀它的尺寸(像 offsetHeight)。瀏覽器為了給出正確答案,被迫當場立刻重算排版——而且如果在迴圈裡反覆「改一下、讀一下」,等於逼主執行緒重排 N 次。連招打到一半一直被自己取消後搖重來,整波團戰就這樣卡死。
Mogu OS:
記住這張對照表,你就贏過一半的前端:
- 動畫要順 → 動
transform/opacity(副手的活,主手在忙也順)- 動畫會卡 → 動
height/top/background-color(綁死主手,每幀重算)- 千萬別做 → 在迴圈裡「改樣式、讀尺寸、改樣式、讀尺寸」(強制同步重排,主手原地爆炸)
這些不是玄學,全部都能用「主執行緒到底有沒有被驚動」一句話推出來。懂了上面那條分界線,這些 best practice 你不用背,自己就推得出來 ╰(°▽°)╯
想讓一個元素滑進畫面、即使主執行緒很忙也順順跑,該動哪個性質?
`transform`(和 `opacity`)只走合成執行緒、不需要重跑排版,所以主執行緒再忙都能 60 幀順跑(B)。`top` 和 `margin-top` 會改變幾何,每一幀都逼忙碌的主執行緒重排(A、C 錯),一卡就掉幀。這就是『能用 `transform` 就別用 `top`』這條金律的底層原因。
正確答案是 B
`transform`(和 `opacity`)只走合成執行緒、不需要重跑排版,所以主執行緒再忙都能 60 幀順跑(B)。`top` 和 `margin-top` 會改變幾何,每一幀都逼忙碌的主執行緒重排(A、C 錯),一卡就掉幀。這就是『能用 `transform` 就別用 `top`』這條金律的底層原因。
🎯 最終魔王:串起來
把上下兩集接起來,看一次完整的團戰。
滑了一下這篇文章。
守線英雄的主執行緒開始比連招前搖:HTML 早就解析成 DOM、CSS 變成 CSSOM、每個元素的樣式算好。接著它排版——算出每個段落、每張圖站在哪、多大;再上色——列出一份「在這裡畫這個」的指令清單。到這裡,它一個人累得半死,但螢幕上還沒半個像素。
然後它把活交給合成執行緒這個副手。副手把頁面切成圖層、切成小圖磚,叫一票點陣化小工平行把指令變成像素,存進 GPU 記憶體,組成一個「合成幀」,送給隊長,最後輸出核心用顯卡把圖層疊好、貼上螢幕。
整套連招,16 毫秒內打完。往下滑的那一下,順順的。
而且因為只是在滑(合成執行緒挪圖層),就算這個頁面背景的 JS 把主執行緒卡得死死的,畫面還是滑得很順——那是兩條命。
這就是瀏覽器的「黑盒」裡,每秒鐘上演 60 次的事。下次打開 DevTools 的效能(Performance)面板,看到那些一條一條的彩色長條——紫色的排版、綠色的上色、那些 16 毫秒一格的方框——就會知道:喔,那不是什麼神秘的色塊,那是這支五人隊一場一場團戰的錄影。
黑盒,拆完了。
延伸閱讀
- 瀏覽器其實是一支五人隊(上):為什麼一個分頁當掉,不會帶走整個瀏覽器(先認人,再看團戰)
- Lv-10:一個 URL 的旅程 — 從按下網址到畫面出現,瀏覽器到底在幹嘛(那篇也講了繪製流程的骨架,這篇補上「為什麼會卡」)
Mogu 插嘴:
上集說「你再也回不去『瀏覽器是一個程式』的純真年代」。這集更慘:你以後每次看到網頁卡卡的,腦中會自動開始抓兇手——是主執行緒被肥 JS 綁死?還是有人在動
height不動transform?恭喜你升級成「看到掉幀就想抓兇手除錯」的那種人。回不去了,真的 (◍˃̶ᗜ˂̶◍)ノ