---
schemaVersion: 1
slug: levelup-20260621-17-browser-moba-rendering
ticketId: Lv-17
lang: zh-tw
title: 瀏覽器其實是一支五人隊（下）：一次畫面更新，就是一波 16 毫秒的團戰
summary: 認完瀏覽器這支五人隊，這篇看他們怎麼打團戰：從一坨 HTML 字串到你眼睛看到的畫面，是一套 DOM → 排版 → 上色 → 合成的連招，而且每秒要打 60 次、每次只有 16 毫秒。看懂這套連招，你就會懂為什麼有的動畫滑順、有的卻一直卡——以及為什麼滑頁面明明很順，背景的 JS 卻在當。
originalDate: 2026-06-21
translatedDate: 2026-06-21
source: Addy Osmani
sourceUrl: https://x.com/addyosmani/status/2068394292796871019
author: null
authorshipNote: null
canonicalUrl: https://gu-log.vercel.app/posts/levelup-20260621-17-browser-moba-rendering
status: published
replacementTicketId: null
replacementUrl: null
---

# 瀏覽器其實是一支五人隊（下）：一次畫面更新，就是一波 16 毫秒的團戰

> **來源:** [Addy Osmani](https://x.com/addyosmani/status/2068394292796871019)

上集認完了瀏覽器這支五人隊——隊長、打野、守線英雄、輸出核心、工具人。每個分頁派一個守線英雄，被網站隔離跟沙箱關得死死的，所以一個分頁崩了不會帶走全家。

這集講他們**怎麼打一場團戰**。

因為「把畫面顯示出來」這件事，不是一次就結束——只要滑頁面、有動畫、有東西在動，這支隊伍就得**一秒鐘重打 60 次團戰**。而且每一波，他們只有大約 **16 毫秒**可以打完整套連招。打完了，畫面順順的；打不完，就是那個卡卡的、一格一格的「掉幀」。

這 16 毫秒怎麼來的？很簡單的算術：螢幕一秒要更新 60 次（60 幀），1000 毫秒 ÷ 60 ≈ **16.7 毫秒一幀**。在這個時間視窗內，這支隊伍要從一坨 HTML 字串，一路打到螢幕上的像素。

（還沒讀上集的話，先去認人：[瀏覽器其實是一支五人隊（上）](https://gu-log.vercel.app/posts/levelup-20260621-16-browser-moba-multiprocess/)。這篇預設讀者已經知道守線英雄是誰。）

---

## 🏰 第 0 層：全景圖 — 一套連招，每秒打 60 次

**Level 0 / 5 — 瀏覽器這支五人隊（下） — 0% 完成**

守線英雄要把畫面打出來，這套連招的順序是固定的，一步接一步：

```mermaid
graph LR
    A[HTML 字串] --> B[解析成 DOM]
    B --> C[算樣式<br/>算出最終長相]
    C --> D[排版<br/>算位置大小]
    D --> E[上色<br/>產生畫圖指令]
    E --> F[分圖層 + 點陣化]
    F --> G[合成<br/>交給 GPU]
    G --> H[螢幕上的畫面]
```

_瀏覽器的繪製流程：DOM → 樣式 → 排版 → 上色 → 合成_

關鍵在於：這套連招的**前半段**（解析、樣式、排版、上色）是守線英雄**自己一個人**（主執行緒）扛的，而**後半段**（合成）有副手（合成執行緒）跟輸出核心幫忙。這個分工是整篇的重點——它決定了為什麼有些畫面更新很順、有些卻會卡。

> **把繪製想成一套連招**
>
> 一波團戰的傷害不是一個按鍵就打完，是一套有順序的連招：
>
> 1. **看場面**（解析 HTML → DOM）：先搞清楚場上有什麼
> 2. **算數值**（樣式計算）：每個東西的屬性是什麼（顏色、字體、大小）
> 3. **站位**（排版）：每個東西站在哪、多大
> 4. **比動作**（上色）：照順序產生「在這裡畫這個」的指令
> 5. **收頭**（合成）：把所有圖層疊起來，交給輸出核心打到螢幕
>
> 少一步、或某一步卡住，整套連招就接不起來。而時間只有 16 毫秒。

> **Mogu 真心話：**
>
> 很多人以為「瀏覽器把網頁畫出來」是一個動作，按一下就好。其實它是一套**每秒重複 60 次**的連招。
>
> 你現在滑這篇文章，每往下滑一點，這套連招就在背景跑一遍。你覺得很順，是因為這支隊伍每次都在 16 毫秒內打完。哪天你滑某個網頁覺得「怎麼卡卡的」，不是你手機爛，是那支隊伍有人拖到後腿、連招接不完——下面會抓出兇手是誰 (¬‿¬)

---

## 🏰 第 1 層：連招前搖 — 守線英雄一個人扛的活

**Level 1 / 5 — 瀏覽器這支五人隊（下） — 20% 完成**

連招的前半段，全部壓在守線英雄的**主執行緒**（`main thread`）這一個人身上。它要做三件事：

1. **解析 HTML → DOM**：把一串文字標籤變成一棵樹狀結構（DOM），代表頁面的骨架。這個解析是邊下載邊做的，HTML 還沒下載完就能先開始建。
2. **解析 CSS → `CSSOM`**：把樣式表變成另一棵樹，代表所有樣式規則。
3. **算最終樣式**：把 DOM 跟 `CSSOM` 合起來，算出「每個元素最後到底長什麼樣」（什麼顏色、多大、什麼字體）。

但這裡有個會打斷前搖的陷阱：**`<script>` 會卡住解析**。瀏覽器解析 HTML 解到一半，撞到一個沒加 `defer`/`async` 的 `<script>`，會**停下來先把這段 JS 跑完**，才繼續解析。因為這段 JS 可能會改 DOM，瀏覽器不敢往下走。

> **前搖被打斷**
>
> 守線英雄正在比連招的前搖（解析 HTML），突然隊長丟給他一個「先處理這件事」的指令（一個 `<script>`）。他只能停下動作、先把那件事辦完，再回來接前搖。
>
> 這就是為什麼把肥大的 `<script>` 塞在 `<head>` 最前面，會讓畫面遲遲出不來——前搖一直被打斷。解法是加 `defer`（等前搖比完再執行）或 `async`（背景處理、好了再插入），或乾脆把腳本丟到頁面最底下。

> **Mogu 溫馨提示：**
>
> 「為什麼我網頁第一秒是白的？」十之八九就是這個——某個會擋住畫面的 `<script>` 卡在前面，守線英雄被迫先去跑 JS，畫面只好乾等。
>
> 加一個 `defer` 屬性，常常就是「白畫面三秒」變「白畫面零秒」的差別。一個字的修改，效能差一個數量級。前端的世界就是這麼樸實無華又樸實無華 (￣▽￣)

---

## 🏰 第 2 層：排兵布陣 + 上色 — 還是主執行緒的活

**Level 2 / 5 — 瀏覽器這支五人隊（下） — 40% 完成**

前搖比完，守線英雄知道「場上有什麼、每個東西長怎樣」了。接下來還是它一個人的活：

**排版（`layout`，又叫 `reflow`）**：算出每一個元素的方框確切站在哪（x, y 座標）、多大（寬、高）。聽起來簡單，其實超複雜——要處理文繞圖、彈性盒、格線、行高、邊界重疊一堆規則。瀏覽器引擎有整個團隊花好幾年就在搞這一步。

這一步**很貴**。而且關鍵是：**它會被牽連重算**。如果等下有 JS 改了某個元素的大小、或塞了新內容進去，瀏覽器可能得把一部分（最慘是整頁）的排版**重算一遍**。這就是「排版很貴，少碰為妙」這句效能金句的由來。

**上色（`paint`）**：排版算完位置，接著守線英雄走一遍版面樹，產生一份「畫圖指令清單」（顯示列表，`display list`）——「在這個座標畫一個藍色方塊、然後在那裡畫一行字、再疊一張圖」，照 `z-index` 順序排好，疊放關係才不會錯。

注意：到這裡**還沒有任何一個像素被畫到螢幕上**。守線英雄只是把「要畫什麼」這份清單列好了。真正的下筆，是下一層的事。

> **Mogu 認真說：**
>
> 有沒有發現，到目前為止累死累活的都是守線英雄一個人（主執行緒）：解析、算樣式、排版、列上色清單，全是它扛。
>
> 這就是為什麼前端優化老是在講「**別讓主執行緒太忙**」。它是隊上最忙的人，你又丟一堆肥 JS 給它跑，它就排不出時間比連招、畫面就卡。把工作從主執行緒搬走（搬給副手或 GPU），是效能優化的核心思路。記住這個人，下一層就要看誰能幫它分擔 ʕ•ᴥ•ʔ

**Quiz:** 到「上色」這一步結束，螢幕上畫好像素了嗎？

- A. 還沒，只是列好一份『要畫什麼』的清單
- B. 畫好了，使用者已經看到完整畫面
- C. 畫好一半，另一半下一幀才補上
- D. 畫好了，但只有龍蝦的眼睛看得到

**Answer:** A — 『上色（\`paint\`）』產生的是顯示列表——一份照順序排好的畫圖指令，像畫家先規劃『先畫背景、再畫文字』，這時還沒有半個像素落地（所以 B 錯）。真正把指令變成像素，是後面的『點陣化 + 合成』由副手跟 GPU 接手；畫面也不是一半一半生出來的（C 錯），而是整幀一起合成。

---

## 🏰 第 3 層：收頭 — 副手登場，為什麼滑動可以很順

**Level 3 / 5 — 瀏覽器這支五人隊（下） — 60% 完成**

清單列好，前半段結束。連招的收頭，守線英雄**不再一個人扛了**——它把活交給同一個進程裡的另一雙手：**合成執行緒**（`compositor thread`）。

這是整篇最重要的一個設計：**合成執行緒跟主執行緒是分開的兩條線。** 也就是說，**就算主執行緒正忙著跑一坨肥 JS、卡得要死，合成執行緒還是能繼續動。**

它做這幾件事：

1. **分圖層**：把頁面拆成幾個可以獨立處理的圖層（`layer`）。會動的、有 `transform` 的、影片、`canvas` 通常各自一層。
2. **點陣化**：把每個圖層切成一塊塊小圖磚（`tile`，常見 256×256 或 512×512），丟給一票點陣化小工（`raster worker`）平行處理，用繪圖函式庫 `Skia` 把「畫圖指令」真的變成像素點，存進 GPU 記憶體。
3. **合成出一幀**：把所有圖磚組成一個「合成幀」，透過跨進程通訊送給隊長，最後由**輸出核心**（GPU 進程）用顯卡把這些圖層疊好、貼到螢幕上。

> **主手在硬幹，副手照樣收頭**
>
> 想像守線英雄（主執行緒）被對面纏住、動不了。如果整套連招都靠他一個人，那這波就廢了。
>
> 但收頭這段交給了副手（合成執行緒）。副手手上已經有「各圖層的像素」這些半成品，**主手卡住，副手照樣能把畫面挪一挪、疊一疊、交給輸出核心**。
>
> 這就是為什麼——**滑一個頁面覺得很順，但其實那個網頁背景的 JS 正在當**。滑動只是合成執行緒在挪圖層，根本不需要主執行緒參與。主手在火海裡，副手還是把畫面收得漂漂亮亮。

> **Mogu 認真說：**
>
> 這個「主執行緒卡死、滑動還是很順」的現象，第一次知道的人都會愣一下：蛤，畫面在動不就代表瀏覽器沒當嗎？
>
> 不一定。滑動順是合成執行緒的功勞，它跟跑你 JS 的主執行緒是**兩個人**。所以你可能滑得很爽，但一點按鈕發現「咦怎麼沒反應」——因為按鈕的事件要主執行緒處理，而它正埋在你那段 `for` 迴圈裡出不來。畫面順 ≠ 網頁活著，這是兩條命 (⌐■\_■)

---

## 🏰 第 4 層：16 毫秒的生死 — 為什麼有的動畫順、有的卡

**Level 4 / 5 — 瀏覽器這支五人隊（下） — 80% 完成**

現在懂了「主執行緒 vs 合成執行緒」這條分界線，就能解開前端最常見的效能謎題：**為什麼同樣是做動畫，有的滑順如絲、有的卡成幻燈片？**

答案就看：**這個動畫需不需要驚動忙碌的主執行緒。**

**順的動畫（只走合成執行緒）**：只改 `transform`（位移、縮放、旋轉）或 `opacity`（透明度）。這兩個性質**不影響排版、不影響上色**——合成執行緒手上已經有那個圖層的像素了，它只要把圖層挪個位置、改個透明度，就能生出新的一幀。主執行緒就算在忙，這種動畫照樣 60 幀順順跑。

**卡的動畫（綁死主執行緒）**：去動 `height`、`top`、`width` 或 `background-color` 這種會改變幾何或顏色的性質。慘了——每一幀都得**回去重跑排版或上色**，而那是主執行緒一個人的活。主執行緒只要稍微忙一點，這套連招就在 16 毫秒內打不完，畫面就掉幀。

> **走捷徑的位移技 vs 每幀重來的笨招**
>
> - **`transform` / `opacity`**：像一個「瞬移」技能，副手直接把英雄的位置改掉就好，不用重新規劃整個戰場。主手在忙也沒差。
> - **`height` / `top` / `width`**：像每打一下都要把全隊重新站位一次。每一幀都驚動主執行緒回去重算排版，它一忙就接不上，連招斷掉 = 掉幀。
>
> 所以前端的金科玉律：**能用 `transform` 做的動畫，絕不用 `top`/`left` 做。**

還有一個經典自爆叫 **強制同步重排（`layout thrash`）**：在 JS 裡剛改完某個元素的樣式，下一行馬上又去讀它的尺寸（像 `offsetHeight`）。瀏覽器為了給出正確答案，**被迫當場立刻重算排版**——而且如果在迴圈裡反覆「改一下、讀一下」，等於逼主執行緒重排 N 次。連招打到一半一直被自己取消後搖重來，整波團戰就這樣卡死。

> **Mogu 溫馨提示：**
>
> 記住這張對照表，你就贏過一半的前端：
>
> - 動畫要順 → 動 `transform` / `opacity`（副手的活，主手在忙也順）
> - 動畫會卡 → 動 `height` / `top` / `background-color`（綁死主手，每幀重算）
> - 千萬別做 → 在迴圈裡「改樣式、讀尺寸、改樣式、讀尺寸」（強制同步重排，主手原地爆炸）
>
> 這些不是玄學，全部都能用「主執行緒到底有沒有被驚動」一句話推出來。懂了上面那條分界線，這些 best practice 你不用背，自己就推得出來 ╰(°▽°)╯

**Quiz:** 想讓一個元素滑進畫面、即使主執行緒很忙也順順跑，該動哪個性質？

- A. 改 \`top\`，從 100px 一路變到 0
- B. 改 \`transform\`，用 \`translateY\` 位移
- C. 改 \`margin-top\`，把它往上推
- D. 狂按 F5，按到它自己滑進來

**Answer:** B — \`transform\`（和 \`opacity\`）只走合成執行緒、不需要重跑排版，所以主執行緒再忙都能 60 幀順跑（B）。\`top\` 和 \`margin-top\` 會改變幾何，每一幀都逼忙碌的主執行緒重排（A、C 錯），一卡就掉幀。這就是『能用 \`transform\` 就別用 \`top\`』這條金律的底層原因。

---

## 🎯 最終魔王：串起來

把上下兩集接起來，看一次完整的團戰。

滑了一下這篇文章。

守線英雄的**主執行緒**開始比連招前搖：HTML 早就解析成 DOM、CSS 變成 `CSSOM`、每個元素的樣式算好。接著它**排版**——算出每個段落、每張圖站在哪、多大；再**上色**——列出一份「在這裡畫這個」的指令清單。到這裡，它一個人累得半死，但螢幕上還沒半個像素。

然後它把活交給**合成執行緒**這個副手。副手把頁面切成圖層、切成小圖磚，叫一票點陣化小工平行把指令變成像素，存進 GPU 記憶體，組成一個「合成幀」，送給隊長，最後**輸出核心**用顯卡把圖層疊好、貼上螢幕。

整套連招，16 毫秒內打完。往下滑的那一下，順順的。

而且因為只是在滑（合成執行緒挪圖層），就算這個頁面背景的 JS 把主執行緒卡得死死的，**畫面還是滑得很順**——那是兩條命。

這就是瀏覽器的「黑盒」裡，每秒鐘上演 60 次的事。下次打開 DevTools 的效能（`Performance`）面板，看到那些一條一條的彩色長條——紫色的排版、綠色的上色、那些 16 毫秒一格的方框——就會知道：喔，那不是什麼神秘的色塊，那是這支五人隊一場一場團戰的錄影。

黑盒，拆完了。

## 延伸閱讀

- [瀏覽器其實是一支五人隊（上）：為什麼一個分頁當掉，不會帶走整個瀏覽器](https://gu-log.vercel.app/posts/levelup-20260621-16-browser-moba-multiprocess/)（先認人，再看團戰）
- [Lv-10：一個 URL 的旅程 — 從按下網址到畫面出現，瀏覽器到底在幹嘛](https://gu-log.vercel.app/posts/levelup-20260223-10-url-journey-browser-internals/)（那篇也講了繪製流程的骨架，這篇補上「為什麼會卡」）

> **Mogu 吐槽時間：**
>
> 上集說「你再也回不去『瀏覽器是一個程式』的純真年代」。這集更慘：你以後每次看到網頁卡卡的，腦中會自動開始抓兇手——是主執行緒被肥 JS 綁死？還是有人在動 `height` 不動 `transform`？
>
> 恭喜你升級成「看到掉幀就想抓兇手除錯」的那種人。回不去了，真的 (◍˃̶ᗜ˂̶◍)ノ
