[譯] JavaScript 如何工作:渲染引擎和效能優化技巧

stormluke發表於2018-04-17

這是探索 JavaScript 及其構建元件專題系列的第 11 篇。在識別和描述核心元素的過程中,我們分享了在構建 SessionStack 時使用的一些經驗法則。SessionStack 是一個需要魯棒且高效能的 JavaScript 應用程式,它幫助使用者實時檢視和重現它們 Web 應用程式的缺陷。

當構建 Web 應用程式時,你不只是編寫獨立執行的 JavaScript 程式碼片段。你編寫的 JavaScript 需要與環境進行互動。理解環境是如何工作的以及它是由什麼組成的,你就能夠構建更好的應用程式,並且能更好地處理應用程式釋出後才會顯現的潛在問題。

[譯] JavaScript 如何工作:渲染引擎和效能優化技巧

那麼,讓我們看看瀏覽器的主要元件有哪些:

  • 使用者介面:包括位址列、後退和前進按鈕、書籤選單等。實際上,它包括了瀏覽器中顯示的絕大部分,除了你看到的網頁本身的那個視窗。
  • 瀏覽器引擎:它處理使用者介面和渲染引擎之間的互動。
  • 渲染引擎:它負責顯示網頁。渲染引擎解析 HTML 和 CSS,並在螢幕上顯示解析的內容。
  • 網路層:諸如 XHR 請求之類的網路呼叫,通過對不同平臺的不同的實現來完成,這些實現位於一個平臺無關的介面之後。我們在本系列的上一篇文章中更詳細地討論了網路層。
  • UI 後端:它用於繪製核心元件(widget),例如核取方塊和視窗。這個後端暴露了一個平臺無關的通用介面。它使用下層的作業系統提供的 UI 方法。
  • JavaScript 引擎:我們在上一篇文章中詳細介紹了這一主題。基本上,這是 JavaScript 執行的地方。
  • 資料持久化層:你的應用可能需要在本地儲存所有資料。其支援的儲存機制包括 localStorageindexDBWebSQLFileSystem

在這篇文章中,我們將關注渲染引擎,因為它負責處理 HTML 和 CSS 的解析和視覺化,這是大多數 JavaScript 應用程式不斷與之互動的地方。

渲染引擎概述

渲染引擎的主要職責是在瀏覽器螢幕上顯示所請求的頁面。

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

不同的渲染引擎

與 JavaScript 引擎類似,不同的瀏覽器也使用不同的渲染引擎。常見的有這些:

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

渲染的過程

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

[譯] JavaScript 如何工作:渲染引擎和效能優化技巧

構建 DOM 樹

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

假設你有以下文字輸入:

<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>
複製程式碼

這個 HTML 的 DOM 樹如下所示:

[譯] JavaScript 如何工作:渲染引擎和效能優化技巧

基本上,每個元素都作為它所包含元素的父節點,這個結構是遞迴的。

構建 CSSOM 樹

CSSOM 指 CSS 物件模型。當瀏覽器構建頁面的 DOM 時,它在 head 中遇到了一個引用外部 theme.css CSS 樣式表的 link 標籤。瀏覽器預計到它可能需要該資源來呈現頁面,所以它立即發出請求。讓我們假設 theme.css 檔案包含以下內容:

body { 
  font-size: 16px;
}

p { 
  font-weight: bold; 
}

span { 
  color: red; 
}

p span { 
  display: none; 
}

img { 
  float: right; 
}
複製程式碼

與 HTML 一樣,引擎需要將 CSS 轉換為瀏覽器可以使用的東西 —— CSSOM。以下是 CSSOM 樹的樣子:

[譯] JavaScript 如何工作:渲染引擎和效能優化技巧

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

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

另外請注意,上面的樹不是完整的 CSSOM 樹,只顯示了我們決定在樣式表中重寫的樣式。每個瀏覽器都提供了一組預設的樣式,也稱為 「使用者代理樣式」——這是我們在未明確指定任何樣式時看到的樣式。我們的樣式會覆蓋這些預設值。

構建渲染樹

HTML 中的檢視指令與 CSSOM 樹中的樣式資料結合在一起用來建立渲染樹

你可能會問什麼是渲染樹。渲染樹是一顆由視覺化元素以它們在螢幕上顯示的順序而構成的樹型結構。它是 HTML 和相應的 CSS 的視覺化表示。此樹的目的是為了以正確的順序繪製內容。

渲染樹中的節點被稱為 Webkit 中的渲染器或渲染物件。

這就是上述 DOM 和 CSSOM 樹的渲染器樹的樣子:

[譯] JavaScript 如何工作:渲染引擎和效能優化技巧

為了構建渲染樹,瀏覽器大致做了如下工作:

  • 從 DOM 樹的根開始,瀏覽器遍歷每個可見節點。某些節點是不可見的(例如 script、meta 等),並且由於它們不需要渲染而被忽略。一些通過 CSS 隱藏的節點也從渲染樹中省略。例如 span 節點 —— 在上面的例子中,它並不存在於渲染樹中,因為我們明確地其上設定了 display: none 屬性。
  • 對於每個可見節點,瀏覽器找到適當的 CSSOM 規則並應用它們。
  • 瀏覽器輸出帶有內容及其計算出的樣式的可見節點

你可以在這裡檢視 RenderObject 的原始碼(在 WebKit 中):github.com/WebKit/webk…

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

class RenderObject : public CachedImageClient {
  // Repaint the entire object.  Called when, e.g., the color of a border changes, or when a border
  // style changes.
  
  Node* node() const { ... }
  
  RenderStyle* style;  // the computed style
  const RenderStyle& style() const;
  
  ...
}
複製程式碼

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

渲染樹的佈局

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

HTML 使用基於流的佈局模型,這意味著大部分時間內它可以在一次遍歷中(single pass)計算出佈局。座標系是相對於根渲染器的,使用左上原點座標。

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

根渲染器的位置是 0,0,並且其尺寸為瀏覽器視窗(也稱為視口)的可見部分的尺寸。

開始佈局過程意味著給出每個節點它應該出現在螢幕上的確切座標。

繪製渲染樹

在這個階段,瀏覽器遍歷渲染器樹,呼叫渲染器的 paint() 方法在螢幕上顯示內容。

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

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

一般來說,瞭解繪圖是一個漸進的過程是很重要的。為了更好的使用者體驗,渲染引擎會嘗試儘快在螢幕上顯示內容。它不會等到所有的 HTML 被分析完畢才開始構建和佈置渲染樹。一小部分內容先被解析並顯示,同時一邊從網路獲取剩下的內容一邊漸進地渲染。

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

當解析器到達 <script> 標籤時,指令碼將被立即解析並執行。文件解析將會被暫停,直到指令碼執行完畢。這意味著該過程是同步的。

如果指令碼是外部的,那麼它首先必須從網路獲取(也是同步的)。所有解析都會停止,直到網路請求完成。

HTML5 新增了一個選項,可以將指令碼標記為非同步,此時指令碼被其他執行緒解析和執行。

優化渲染效能

如果你想優化你的應用,那麼你需要關注五個主要方面。這些是您可以控制的地方:

  1. JavaScript —— 在之前的文章中,我們介紹了關於編寫高效能程式碼的主題,這些程式碼不會阻塞 UI,並且記憶體效率高等等。當涉及渲染時,我們需要考慮 JavaScript 程式碼與頁面上 DOM 元素互動的方式。JavaScript 可以在 UI 中產生大量的更新,尤其是在 SPA 中。
  2. 樣式計算 —— 這是基於匹配選擇器確定哪個 CSS 規則適用於哪個元素的過程。一旦定義了規則,就會應用這些規則,並計算出每個元素的最終樣式。
  3. 佈局 —— 一旦瀏覽器知道哪些規則適用於元素,就可以開始計算後者佔用的空間以及它在瀏覽器螢幕上的位置。Web 的佈局模型定義了一個元素可以影響其他元素。例如,<body> 的寬度會影響子元素的寬度等等。這一切都意味著佈局過程是計算密集型的。該繪圖是在多個圖層完成的。
  4. 繪圖 —— 這裡開始填充實際的畫素。該過程包括繪製文字、顏色、影象、邊框、陰影等 —— 每個元素的每個視覺部分。
  5. 合成 —— 由於頁面部件被劃分為多層,因此需要按照正確的順序將其繪製到螢幕上,以便正確地渲染頁面。這非常重要,特別是對於重疊元素來說。

優化你的 JavaScript

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

以下是關於可以優化 JavaScript 哪些部分來改善渲染效能的一些小提示:

  • 避免使用 setTimeoutsetInterval 進行檢視更新。這些將在幀中某個不確定的時間點上呼叫 callback,可能在最後。我們想要做的是在幀開始時觸發視覺變化而不是錯過它。
  • 將長時間執行的 JavaScript 計算任務移到 Web Workers 上,像我們之前討論過的那樣
  • 使用微任務在多個幀中變更 DOM。這是為了處理在 Web Worker 中的任務需要訪問 DOM,而 Web Worker 又不允許訪問 DOM 的情況。就是說你可以將一個大任務分解為小任務,並根據任務的性質在 requestAnimationFramesetTimeoutsetInterval 中執行它們。

優化你的 CSS

通過新增和刪除元素、更改屬性等來修改 DOM 會導致瀏覽器重新計算元素樣式,並且在很多情況下還會重新佈局整個頁面或至少其中的一部分。

要優化渲染效能,請考慮以下方法:

  • 減少選擇器的複雜性。相對於構建樣式本身的工作,複雜的選擇器可能會讓計算元素樣式所需的時間增加 50%。
  • 減少必須計算樣式的元素的數量。本質上,直接對幾個元素進行樣式更改,而不是使整個頁面無效。

優化佈局

佈局的重新計算會對瀏覽器造成很大壓力。請考慮下面的優化:

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

優化繪圖

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

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

渲染是 SessionStack 執行的重點之一。當使用者瀏覽你的 web 應用遇到問題時,SessionStack 必須將這些遇到的問題重建成一個視訊。為了做到這點,SessionStack 僅利用我們的庫收集到資料:使用者事件、DOM 更改、網路請求、異常和除錯訊息等。我們的播放器經過高度優化,能夠按順序正確呈現和使用所有收集到的資料,從視覺和技術兩方面為你提供使用者在瀏覽器中發生的一切的畫素級完美模擬。

如果你想試試看,這裡可以免費嘗試 SessionStack

[譯] JavaScript 如何工作:渲染引擎和效能優化技巧

資源


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章