瀏覽器的本質工作是:把一個 URL 變成螢幕上顯示的網頁

理解這個過程,對效能最佳化與錯誤排查都有很大的幫助。

瀏覽器渲染流程#

flowchart TD
    A[URL] --> B[HTTP 請求]
    B --> C[HTML 解析]
    C --> D[DOM 樹]
    D --> E[CSS 計算]
    E --> F[CSSOM]
    F --> G[渲染樹]
    G --> H[佈局 Layout]
    H --> I[繪製 Paint]
    I --> J[合成 Composite]
    J --> K[顯示到螢幕]

    style A fill:#e3f2fd
    style K fill:#c8e6c9

這個過程是流式處理的:不需要等到上一步完全結束,就可以開始處理上一步的輸出。

這也是為什麼我們會看到網頁逐步出現的原因。

HTTP 協定#

請求結構#

GET /path HTTP/1.1          ← Request Line
Host: example.com           ← Headers
Content-Type: application/json
                            ← 空行
{ "key": "value" }          ← Body

回應結構#

HTTP/1.1 200 OK             ← Response Line
Content-Type: text/html     ← Headers
Content-Length: 1234
                            ← 空行
<!DOCTYPE html>...          ← Body

HTTP 方法#

方法用途
GET取得資源(瀏覽器網址列)
POST提交資料(表單送出)
PUT新增/替換資源
DELETE刪除資源
HEAD只取得回應標頭
OPTIONS預檢請求(CORS)

HTTP 狀態碼#

範圍含義常見狀態碼
1xx臨時回應100 Continue
2xx成功200 OK
3xx重新導向301 永久轉址, 302 暫時轉址, 304 未修改
4xx用戶端錯誤400 Bad Request, 403 Forbidden, 404 Not Found
5xx伺服器端錯誤500 Internal Error, 503 Service Unavailable

304 Not Modified 是前端必知必會的狀態碼。

當用戶端已有快取版本,且伺服器確認沒有更新時,會回傳不含 body 的 304 回應。

HTTPS#

HTTPS = HTTP + TLS 加密層

  • 身份驗證:確認伺服器身份
  • 資料加密:防止中間人竊聽或竄改

HTTP/2 改進#

  1. 伺服器推送:提前推送資源到用戶端快取
  2. TCP 連線複用:多個 HTTP 請求共用一個 TCP 連線
  3. 標頭壓縮:使用 HPACK 演算法壓縮

DOM 樹建構#

文件物件模型(DOM, Document Object Model)就是瀏覽器將 HTML 文件解析後得到的樹狀結構。

HTML 解析過程:

位元組流 → 字元流 → 詞法分析(Token) → 語法分析 → DOM 樹

解析範例#

<html>
  <head>
    <title>Example</title>
  </head>
  <body>
    <p>Hello</p>
  </body>
</html>
graph TD
    DOC[Document] --> HTML[html]
    HTML --> HEAD[head]
    HTML --> BODY[body]
    HEAD --> TITLE[title]
    TITLE --> T1["Example"]
    BODY --> P[p]
    P --> T2["Hello"]

    style DOC fill:#e1f5fe
    style T1 fill:#fff9c4
    style T2 fill:#fff9c4

CSSOM#

CSS 物件模型(CSSOM, CSS Object Model)是串接樣式表(CSS, Cascading Style Sheets)的物件表示,與 DOM 類似但彼此獨立。

取得元素樣式#

// 取得計算後的樣式
const styles = window.getComputedStyle(element);
const width = styles.width;

// 取得元素位置
const rect = element.getBoundingClientRect();
// rect: { x, y, width, height, top, right, bottom, left }

重排與重繪#

重排(Reflow/Layout)#

元素的幾何屬性變化會觸發重排:

  • 新增/刪除可見的 DOM 元素
  • 元素位置、尺寸變化
  • 內容變化(如文字數量)
  • 頁面首次渲染
  • 瀏覽器視窗大小變化

重繪(Repaint)#

元素外觀變化但不影響排版時觸發重繪:

  • 顏色變化
  • 背景變化
  • 可見性變化

重排比重繪的代價更高

因為重排必然伴隨重繪,但重繪不一定會觸發重排。

最佳化建議#

// 不好:多次重排
element.style.width = "100px";
element.style.height = "100px";
element.style.margin = "10px";

// 好:一次性修改
element.style.cssText = "width: 100px; height: 100px; margin: 10px;";

// 或使用 class
element.className = "new-styles";
更多最佳化技巧
  1. 批次 DOM 操作

    // 使用 DocumentFragment
    const fragment = document.createDocumentFragment();
    items.forEach((item) => {
      const li = document.createElement("li");
      li.textContent = item;
      fragment.appendChild(li);
    });
    list.appendChild(fragment);
  2. 離線 DOM 操作

    element.style.display = "none";
    // 進行多次 DOM 操作
    element.style.display = "block";
  3. 避免強制同步排版

    // 不好:讀寫交錯
    element.style.width = "100px";
    const width = element.offsetWidth; // 強制同步排版
    element.style.height = width + "px";
    
    // 好:先讀後寫
    const width = element.offsetWidth;
    element.style.width = "100px";
    element.style.height = width + "px";

事件機制#

捕獲與冒泡#

捕獲是電腦處理事件的邏輯,冒泡是人類理解事件的邏輯。

當你點擊一個按鈕時:

  • 滑鼠只知道座標位置
  • 瀏覽器需要找出這個座標對應的元素,這就是 捕獲過程(從外到內)
  • 同時,點擊按鈕也代表點擊了按鈕所在的容器,這就是 冒泡過程(從內到外)

事件傳播順序#

<body>
  <div id="outer">
    <button id="inner">Click</button>
  </div>
</body>
// 捕獲階段(第三個參數為 true)
document.body.addEventListener(
  "click",
  () => {
    console.log("body capture");
  },
  true
);

document.getElementById("outer").addEventListener(
  "click",
  () => {
    console.log("outer capture");
  },
  true
);

// 冒泡階段(第三個參數為 false 或省略)
document.getElementById("inner").addEventListener("click", () => {
  console.log("inner");
});

document.getElementById("outer").addEventListener("click", () => {
  console.log("outer bubble");
});

document.body.addEventListener("click", () => {
  console.log("body bubble");
});

// 點擊 button 的輸出順序:
// body capture → outer capture → inner → outer bubble → body bubble
sequenceDiagram
    participant body
    participant outer as outer div
    participant inner as inner button

    Note over body,inner: 捕獲階段(外 → 內)
    body->>outer: body capture
    outer->>inner: outer capture

    Note over inner: 目標階段
    inner->>inner: inner (target)

    Note over body,inner: 冒泡階段(內 → 外)
    inner->>outer: outer bubble
    outer->>body: body bubble

addEventListener 參數#

element.addEventListener(type, handler, options);

// options 可以是布林值(useCapture)
element.addEventListener("click", handler, true); // 捕獲
element.addEventListener("click", handler, false); // 冒泡(預設)

// 或者是物件
element.addEventListener("click", handler, {
  capture: false, // 是否捕獲
  once: true, // 只執行一次
  passive: true, // 承諾不會呼叫 preventDefault()
});

實際開發建議

  • 預設使用冒泡模式(不傳第三個參數)
  • 開發元件時,若需要父元素控制子元素行為時使用捕獲

焦點系統#

焦點系統負責處理鍵盤事件。

整個使用者介面(UI, User Interface)在任何時刻,都有且僅有一個「聚焦」的元素。

// 設定焦點
element.focus();

// 移除焦點
element.blur();

// Tab 鍵用於切換焦點
// 可以透過 tabindex 屬性控制 Tab 順序

自訂事件#

// 建立自訂事件
const event = new CustomEvent("my-event", {
  bubbles: true, // 是否冒泡
  cancelable: true, // 是否可取消
  detail: { data: "custom data" }, // 自訂資料
});

// 監聽事件
element.addEventListener("my-event", (e) => {
  console.log(e.detail.data);
});

// 觸發事件
element.dispatchEvent(event);

常用瀏覽器 API#

DOM 操作#

// 查詢
document.getElementById("id");
document.querySelector(".class");
document.querySelectorAll("div");

// 建立
document.createElement("div");
document.createTextNode("text");
document.createDocumentFragment();

// 修改
element.appendChild(child);
element.removeChild(child);
element.replaceChild(newChild, oldChild);
element.insertBefore(newChild, referenceChild);

// 屬性
element.getAttribute("data-id");
element.setAttribute("data-id", "123");
element.removeAttribute("data-id");

// 類名
element.classList.add("active");
element.classList.remove("active");
element.classList.toggle("active");
element.classList.contains("active");

位置與尺寸#

// 元素相對於視口的位置
element.getBoundingClientRect();

// 滾動位置
window.scrollX; // 水平滾動距離
window.scrollY; // 垂直滾動距離

// 元素滾動
element.scrollTop;
element.scrollLeft;

// 滾動到指定位置
element.scrollTo({ top: 100, behavior: "smooth" });
element.scrollIntoView({ behavior: "smooth" });

Performance API#

// 頁面載入效能
const timing = performance.timing;
const loadTime = timing.loadEventEnd - timing.navigationStart;

// 效能標記
performance.mark("start");
// ... 程式碼執行
performance.mark("end");
performance.measure("duration", "start", "end");

// 取得量測結果
const measures = performance.getEntriesByType("measure");
console.log(measures[0].duration);

總結#

概念要點
渲染流程串流處理,HTML ➡️ DOM ➡️ CSSOM ➡️ 渲染 ➡️ 繪製
HTTP請求-回應模式,注意狀態碼和快取機制
重排重繪重排代價高,應批次操作 DOM
事件機制捕獲(外 ➡️ 內)➡️ 冒泡(內 ➡️ 外)
焦點系統處理鍵盤事件,Tab 鍵切換