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

摘要: 理解瀏覽器渲染。

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

這是專門探索 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

Fundebug專注於JavaScript、微信小程序、微信小遊戲、支付寶小程序、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了9億+錯誤事件,付費客戶有Google、360、金山軟件、百姓網等衆多品牌企業。歡迎大家免費試用