在現代 Web 開發中,了解瀏覽器如何渲染頁面以及 JavaScript 如何影響頁面加載流程可以更好地理解前端開發的核心原理。

一、瀏覽器渲染進程概述

瀏覽器的渲染進程(Render 進程)主要負責頁面的渲染、腳本執行和事件處理等工作。為了避免因單個頁面崩潰而導致整個瀏覽器崩潰,每個頁面都有獨立的渲染進程。

Render 進程是一個多線程架構,包含以下主要線程:

  1. GUI 渲染線程

    1. 負責渲染瀏覽器界面,包括解析 HTML 和 CSS,構建 DOM 樹和 RenderObject 樹,以及執行布局和繪制等操作。

    2. 當界面需要重繪(Repaint)或由于操作引發回流(Reflow)時,該線程會被激活。

    3. 注意:GUI 渲染線程與 JavaScript 引擎線程是互斥的。當 JavaScript 引擎執行時,GUI 線程會被掛起,所有需要更新的 GUI 操作會被暫存到一個隊列中,只有當 JavaScript 引擎空閑時,這些操作才會被執行。

  2. JS 引擎線程

    1. 也稱為 JavaScript 內核,如 V8 引擎,負責處理 JavaScript 腳本程序。

    2. 單線程特性 :JavaScript 主線程一次只能執行一個任務(同步代碼),同步任務按順序依次執行,不會阻塞其他 JavaScript 代碼的執行。

    3. 任務隊列(Task Queue) :異步任務的回調函數會被放置在這個隊列中。有兩種類型的任務隊列:

      • 宏任務(macrotask)隊列 :例如 setTimeoutsetInterval 的回調,網絡請求的回調, Document Object 的事件等。

      • 微任務(microtask)隊列 :由 PromiseMutationObserverprocess.nextTick(Node.js 環境) 等生成的回調。

    4. 事件循環(Event Loop)機制

      • 執行規則 :事件循環不斷檢測主線程是否空閑,如果空閑就從任務隊列中取出任務執行。

      • 工作過程

        • 瀏覽器環境或 Node.js 環境中,JavaScript 引擎(如 V8)執行同步代碼,完成主線程上的所有同步任務。

        • 如果遇到異步任務(如 setTimeoutsetInterval 的回調),它們會被暫時擱置在相應的任務隊列中。

        • 引擎會監聽宏任務和微任務隊列中的任務:

          1. 微任務隊列 的優先級高于 宏任務隊列。在每次執行完主線程的一個同步任務之后,會先檢查微任務隊列,按順序執行所有微任務,直到微任務隊列空。

          2. 然后引擎會檢查宏任務隊列,取出一個任務執行。

  3. 事件觸發線程

    1. 歸屬于瀏覽器而非 JavaScript 引擎,用于控制事件循環。

    2. 當執行諸如 Promise 等操作時,相關任務會被添加到事件線程中。當事件觸發條件滿足時,線程會將事件添加到待處理隊列的隊尾,等待 JavaScript 引擎的處理。

  4. 定時觸發器線程

    1. 專門用于處理 setTimeoutsetInterval 等定時任務。

    2. 瀏覽器的定時計數器不依賴 JavaScript 引擎計數(因為 JavaScript 引擎是單線程的,若處于阻塞狀態會影響計時的準確性)。當定時觸發器線程計時完成后,會通知事件觸發線程,將定時任務的回調函數添加到事件隊列的隊尾。

    3. 后臺定時觸發器線程計時的準確性問題:

      • 當瀏覽器處于后臺時,為了節省系統資源和電量消耗,瀏覽器可能會對定時觸發器線程的執行頻率進行優化調整。將 setTimeoutsetInterval 的執行間隔延長,導致原本應該按照設定時間間隔執行的任務被延遲執行,從而出現計時不準確現象,如果有需要定時觸發器線程要必須在后臺執行強需求的話可以使用。

        • 開啟JS多線程web weoker,倒計時寫在weborker里時,頁面的tab不會影響到倒計時的計算
          let webWorkDate = 100,
            date = 100;
          // 開啟線程
          const work = new Worker('worker.js');
          setInterval(() => {
            date--;
            console.log('普通倒計數:', date);
          }, 1000);
          // 傳輸數據
          work.postMessage({ time: webWorkDate });
          console.log(work);
          // 監聽線程
          work.onmessage = (event) => {
            console.log();
            console.log('Worker倒計數:', event.data.num);
            if (event.data.num === 0) {
              work.terminate(); //關閉線程
            }
          };
          //worker.js
          self.addEventListener(
            'message',
            function (e) {
              setInterval(() => {
                let num = e.data.time--;
                self.postMessage({ num });
              }, 1000);
            },
            false
          );
        
  5. 異步 HTTP 請求線程

    1. 在使用 XMLHttpRequestfetch 等進行網絡請求時,瀏覽器會開啟一個新的線程負責請求。

    2. 當檢測到狀態變更時,若設置有回調函數,異步線程會生成狀態變更事件,并將回調函數放入事件隊列中,等待 JavaScript 引擎執行。


二、頁面加載的整體執行步驟

  1. 加載整體 HTML 文件

    1. 瀏覽器首先會加載整個 HTML 文件,解析其結構。
  2. 解析 HTML 并建立 DOM

    1. 瀏覽器會從上到下解析 HTML 文件,構建 DOM 樹。在解析過程中,遇到諸如 <script><link> 等標簽時,會下載和解析相應的內容。

    2. 如果是 <link> 標簽,瀏覽器會解析 CSS 文件并構建 CSS 對象模型(CSSOM 樹)。

  3. 結合 DOM 和 CSSOM 樹生成 Render 樹

    1. Render 樹是 DOM 樹和 CSSOM 樹的結合體,用于描述頁面中可見元素的布局和樣式信息。
  4. 布局 Render 樹(Layout/Reflow)

    1. 負責計算各元素的尺寸和位置等布局信息。
  5. 繪制 Render 樹(Paint)

    1. 根據 Render 樹中的信息,繪制頁面的像素內容。
  6. GPU 合成

    1. 瀏覽器會將各層的信息發送給 GPU,GPU 會將各層合成,并顯示在屏幕上。

三、HTML、CSS 和 JavaScript 的解析與執行

  1. HTML 解析

    1. 瀏覽器從上到下解析 HTML 文件,構建 DOM 樹。

    2. 遇到 <script> 標簽時,若腳本是內部腳本,瀏覽器會立即解析并執行;若是外部腳本,瀏覽器會暫停解析 HTML,等待腳本下載完成后執行。

  2. CSS 解析

    1. CSS 有三種聲明方式:外聯樣式表、內聯樣式表和內部樣式表。瀏覽器會根據這些樣式構建 CSSOM 樹,用于渲染頁面的樣式。
  3. JavaScript 解析

    1. JavaScript 引擎負責解析和執行 JavaScript 腳本。執行 JavaScript 腳本時會阻塞 HTML 解析,因此建議將 <script> 標簽放置在頁面底部,以減少對頁面加載的影響。

四、DOM 文檔加載步驟

  1. 解析 HTML 結構:瀏覽器解析 HTML 文件,構建 DOM 樹。

  2. 加載外部腳本和樣式文件:加載外部的 JavaScript 和 CSS 文件。

  3. 解析并執行腳本代碼:解析和執行 JavaScript 腳本。

  4. 執行事件綁定代碼:如 $(function(){}) 中的代碼。

  5. 加載二進制資源:加載圖片等二進制資源。

  6. 頁面加載完畢:執行 window.onload 事件。

希望本文能幫助你更深入地理解瀏覽器的工作原理,關于如何優化頁面加載性能咱們下回接著再聊。