瀏覽器的工作本質上是:把一個 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 樹構建#

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 Object Model 是 CSS 的物件表示,與 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 操作**

   ```javascript
   element.style.display = "none";
   // 進行多次 DOM 操作
   element.style.display = "block";
  1. 避免強制同步佈局

    // 不好:讀寫交錯
    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";

</div></details>

## 事件機制

### 捕獲與冒泡

> [!IMPORTANT]
>
> **捕獲是計算機處理事件的邏輯,冒泡是人類理解事件的邏輯。**

當你點擊一個按鈕時:

- 滑鼠只知道座標位置
- 瀏覽器需要找出這個座標對應哪個元素 → **捕獲過程**(從外到內)
- 同時,點擊按鈕也意味著點擊了按鈕所在的容器 → **冒泡過程**(從內到外)

### 事件傳播順序

```html
<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 中有且僅有一個「聚焦」的元素。

// 設置焦點
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 鍵切換