JavaScript是如何工作的:渲染引擎和優化其效能的技巧

Fundebug發表於2019-01-14

摘要: 理解瀏覽器渲染。
這是專門探索 JavaScript 及其所構建的元件的系列文章的第11篇。

如果你錯過了前面的章節,可以在這裡找到它們:

當你構建 Web 應用程式時,你不只是編寫單獨執行的 JavaScript 程式碼,你編寫的 JavaScript 正在與環境進行互動。瞭解這種環境,它的工作原理以及它的組,這些有助於你夠構建更好的應用程式,併為應用程式釋出後可能出現的潛在問題做好充分準備。

瀏覽器的主要元件包括:

  • 使用者介面 (User interface): 包括位址列、後退/前進按鈕、書籤目錄等,也就是你所看到的除了用來顯示你所請求頁面的主視窗之外的其他部分
  • 瀏覽器引擎 (Browser engine):用來查詢及操作渲染引擎的介面
  • 渲染引擎 (Rendering engine):用來顯示請求的內容,例如,如果請求內容為 html,它負責解析 html 及 css,並將解析後的結果顯示出來
  • 網路 (Networking):用來完成網路呼叫,例如http請求,它具有平臺無關的介面,可以在不同平臺上工作
  • UI 後端 (UI backend):用來繪製類似組合選擇框及對話方塊等基本元件,具有不特定於某個平臺的通用介面,底層使用作業系統的使用者介面
  • JS 直譯器 (JavaScript engine):用來解釋執行JS程式碼
  • 資料儲存 (Data persistence): 屬於持久層,瀏覽器需要在硬碟中儲存類似 cookie 的各種資料,HTML5定義了 Web Database 技術,這是一種輕量級完整的客戶端儲存技術,支援的儲存機制型別包括 localStorageindexDBWebSQLFileSystem

在這篇文章中,將重點討論渲染引擎,因為它處理 HTML 和 CSS 的解析和視覺化,這是大多數 JavaScript 應用程式經常與之互動的東西。

渲染引擎概述

渲染引擎的職責就是渲染,即在瀏覽器視窗中顯示所請求的內容。

渲染引擎可以顯示 HTML 和 XML 文件和影像。如果使用其他外掛,渲染引擎還可以顯示不同型別的文件,如 PDF。

渲染引擎 (Rendering engines)

與 JavaScript 引擎類似,不同的瀏覽器也使用不同的渲染引擎。以下是一些最受歡迎的:

  • Gecko — Firefox
  • WebKit — Safari
  • Blink — Chrome,Opera (版本 15 之後)

Firefox、Chrome 和 Safari 是基於兩種渲染引擎構建的,Firefox 使用 Geoko——Mozilla 自主研發的渲染引擎,Safari 和 Chrome 都使用 Webkit。Blink 是 Chrome 基於 WebKit的自主渲染引擎。

渲染的過程

渲染引擎從網路層接收所請求文件的內容。

解析 HTML 以構建 Dom 樹 -> 構建 Render 樹 -> 佈局 Render 樹 -> 繪製 Render 樹

構建 Dom 樹

渲染現引擎的第一步是解析 HTML文件,並將解析後的元素轉換為 DOM 樹中的實際 DOM 節點。

假如有如下 Html 結構

<html>
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" href="theme.css">
  </head>
  <body>
    <p> Hello, <span> friend! </span> </p>
    <div> 
      <img src="smiley.gif" alt="Smiley face" height="42" width="42">
    </div>
  </body>
</html>

對應的 DOM 樹如下:

基本上,每個元素都表示為所有元素的父節點,這些元素直接包含在元素中。

構建 CSSOM

CSSOM 指的是 CSS 物件模型。 當瀏覽器構建頁面的 DOM 時,它在 head 標籤下如遇到了一個 link 標記且引用了外部 theme.css CSS 樣式表。 瀏覽器預計可能需要該資源來呈現頁面,它會立即傳送請求。 假設 theme.css 檔案內容如下:

body { 
  font-size: 16px;
}

p { 
  font-weight: bold; 
}

span { 
  color: red; 
}

p span { 
  display: none; 
}

img { 
  float: right; 
}

與 HTML一樣,渲染引擎需要將 CSS 轉換成瀏覽器可以使用的東西—— CSSOM。CSSOM 結構如下:

你想知道為什麼 CSSOM 是一個樹形結構? 在為頁面上的任何物件計算最終樣式集時,瀏覽器以適用於該節點的最常規規則開始(例如,如果它是 body 元素的子元素,則應用所有 body 樣式),然後遞迴地細化,通過應用更具體的規則來計算樣式。

來看看具體的例子。包含在 body 元素內的 span 標籤中的任何文字的字型大小均為 16 畫素,並且為紅色。這些樣式是從 body 元素繼承而來的。 如果一個 span 元素是一個 p 元素的子元素,那麼它的內容就不會被顯示,因為它被應用了更具體的樣式(display: none)。

另請注意,上面的樹不是完整的 CSSOM 樹,只顯示我們決定在樣式表中覆蓋的樣式。 每個瀏覽器都提供一組預設樣式,也稱為“user agent stylesheet”。這是我們在未明確指定任何樣式時看到的樣式,我們的樣式會覆蓋這些預設值。

不同瀏覽器對於相同元素的預設樣式並不一致,這也是為什麼我們在 CSS 的最開始要寫 *{padding:0;marging:0};,也就是我們要重置CSS預設樣式的。

構建渲染樹

CSSOM 樹和 DOM 樹連線在一起形成一個 render tree,渲染樹用來計算可見元素的佈局並且作為將畫素渲染到螢幕上的過程的輸入。

  • DOM 樹和 CSSOM 樹連線在一起形成 render tree .
  • render tree 只包含了用於渲染頁面的節點
  • 佈局計算了每一個物件的準確的位置以及大小
  • 繪畫是最後一步,繪畫要求利用 render tree 來將畫素顯示到螢幕上

渲染樹中的每個節點在 Webkit 中稱為渲染器或渲染物件。

收下是上面 DOM 和 CSSOM 樹的渲染器樹的樣子:

為了構建渲染樹,瀏覽器大致執行以下操作:

  • 從 DOM 樹根節點開始,遍歷每一個可見的節點

    • 一些節點是完全不可見的(比如 script標籤,meta標籤等),這些節點會被忽略,因為他們不會影響渲染的輸出
    • 一些節點是通過 CSS 樣式隱藏了,這些節點同樣被忽略——例如上例中的 span 節點在 render tree 中被忽略,因為 span 樣式是 display:none
  • 對每一個可見的節點,找到合適的匹配的CSSOM規則,並且應用樣式
  • 顯示可見節點(節點包括內容和被計算的樣式)

“visibility:hidden”“display:none” 之間的不同,“visibility:hidden” 將元素設定為不可見,但是同樣在佈局上佔領一定空間(例如,它會被渲染成為空盒子),但是 “display:none” 的元素是將節點從整個 render tree 中移除,所以不是佈局中的一部分 。

你可以在這裡檢視 RenderObject 的原始碼(在 WebKit 中):

https://github.com/WebKit/web…

我們來看看這個類的一些核心內容:

每個渲染器代表一個矩形區域,通常對應於一個節點的 CSS 盒模型。它包含幾何資訊,例如寬度、高度和位置。

程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具Fundebug

渲染樹的佈局

建立渲染器並將其新增到樹中時,它沒有位置和大小,計算這些值稱為佈局。

HTML使用基於流的佈局模型,這意味著大多數時間它可以一次性計算幾何圖形。座標系統相對於根渲染器,使用左上原點座標。

佈局是一個遞迴過程 – 它從根渲染器開始,它對應於 HTML 文件的 <html> 元素。 佈局以遞迴方式繼續通過部件或整個渲染器層次結構,為每個需要它的渲染器計算幾何資訊。

根渲染器的位置為0,0,其尺寸與瀏覽器視窗的可見部分(即viewport)的大小相同。開始佈局過程意味著給每個節點在螢幕上應該出現的確切座標。

繪製渲染樹

在此繪製,遍歷渲染器樹並呼叫渲染器的 paint() 方法以在螢幕上顯示內容。

繪圖可以是全域性的或增量式的(與佈局類似):

  • 全域性 — 整棵樹被重繪
  • 增量式 — 只有一些渲染器以不影響整個樹的方式改變。 渲染器使其在螢幕上的矩形無效,這會導致作業系統將其視為需要重新繪製並生成繪 paint 事件的區域。 作業系統通過將多個區域合併為一個來智慧完成。

總的來說,重要的中要理解繪圖是一個漸進的過程。為了更好的使用者體驗,渲染引擎將盡可能快地在螢幕上顯示內容。它不會等到解析完所有 HTML 後才開始構建和佈局渲染樹,而是解析和顯示部分內容,同時繼續處理來自網路的其餘內容項。

處理指令碼和樣式表的順序

當解析器到達 <script> 標記時,將立即解析並執行指令碼。文件的解析將暫停,直到執行指令碼為止。這意味著這個過程是同步的

如果指令碼是外部的,那麼首先必須從網路中獲取它(也是同步的)。所有解析都停止,直到獲取完成。HTML5 新加了async 或 defer 屬性,將指令碼標記為非同步的,以便由不同的執行緒解析和執行。

優化渲染效能

如果你想優化自己的應用,則需要關注五個主要方面,這些是你自己可以控制的:

  1. JavaScript   — 在之前的文章中,討論瞭如果編寫優化程式碼的主題抱包括如果編寫程式碼才不會阻止UI,和提高記憶體利用等等。在渲染時,需要考慮 JavaScript 程式碼與頁面 上DOM 素互動的方式。 JavaScript 可以在 UI中建立大量更改,尤其是在 SPA 中。
  2. 樣式計算 — 這是根據匹配選擇器確定哪個 CSS 規則適用於哪個元素的過程。 定義規則後,將應用它們並計算每個元素的最終樣式。
  3. 佈局 — 一旦瀏覽器知道哪些規則適用於某個元素,它就可以開始計算後者佔用多少空間以及它在瀏覽器螢幕上的位置。Web 的佈局模型定義了一個元素可以影響其他元素。例如, 的寬度會影響其子元素的寬度,等等。這意味著佈局過程是計算密集型的,該繪圖是在多個圖層完成的。
  4. 繪圖 —— 這是實際畫素被填充的地方,這個過程包括繪製文字、顏色、影像、邊框、陰影等——每個元素的每個可視部分。
  5. 合成  — 由於頁面部分可能被繪製成多個層,因此它們需要以正確的順序繪製到螢幕上,以便頁面渲染正確。這是非常重要的,特別是對於重疊的元素。

優化你的 JavaScript

JavaScript 經常觸發瀏覽器中的視覺變化,構建 SPA 時更是如此。

以下是一些優化 JavaScript 渲染技巧:

  • 避免使用 setTimeoutsetInterval 進行可視更新。 這些將在幀中的某個點呼叫 callback ,可能在最後。我們想要做的是在幀開始時觸發視覺變化而不是錯過它。
  • 之前文章 所述,將長時間執行的 JavaScript 計算轉移到 Web Workers。
  • 使用微任務在多個幀中變更 DOM。這是在任務需要訪問 DOM 時使用的, Web Worker 無法訪問 DOM。這基本上意味著你要把一個大任務分解成更小的任務,然後根據任務的性質在 requestAnimationFrame, setTimeout, setInterval 中執行它們。

優化你的 CSS

通過新增和刪除元素,更改屬性等來修改 DOM 將使瀏覽器重新計算元素樣式,並且在許多情況下,重新計算整個頁面的佈局或至少部分佈局。

要優化渲染,考慮以下事項:

  • 減少選擇器的複雜性,與構造樣式本身的其他工作相比,選擇器複雜性可以佔用計算元素樣式所需時間的50%以上。

* 減少必須進行樣式計算的元素的數量。本質上,直接對一些元素進行樣式更改,而不是使整個頁面無效。

優化佈局

瀏覽器的佈局重新計算可能非常繁重。 考慮以下優化:

  • 儘可能減少佈局的數量。當你更改樣式時,瀏覽器會檢查是否有任何更改需要重新計算佈局。對寬度、高度、左、頂等屬性的更改,以及通常與幾何相關的屬性的更改,都需要佈局。所以,儘量避免改變它們。
  • 儘量使用 flexbox 而不是老的佈局模型。它執行速度更快,可為你的應用程式創造巨大的效能優勢。
  • 避免強制同步佈局。需要記住的是,在 JavaScript 執行時,前一幀中的所有舊佈局值都是已知的,可以查詢。如果你訪問 box.offsetHeight,那就不成問題了。但是,如果你在訪問 box 之前更改了它的樣式(例如,通過動態地向元素新增一些 CSS 類),瀏覽器必須先應用樣式更改並執行佈局過程,這是非常耗時和耗費資源的,所以儘可能避免。

優化繪圖

這通常是所有任務中執行時間最長的,因此儘可能避免這種情況非常重要。 以下是我們可以做的事情:

  • 除了變換(transform)和透明度之外,改變其他任何屬性都會觸發重新繪圖,請謹慎使用。
  • 如果觸發了佈局,那也會觸發繪圖,因為更改佈局會導致元素的視覺效果也改變。
  • 通過圖層提升和動畫編排來減少重繪區域。

原文:

https://blog.sessionstack.com…

Fundebug經授權轉載,版權歸原作者所有。


相關文章