How Javascript works (Javascript工做原理) (十一) 渲染引擎及性能優化小技巧

我的總結:讀完這篇文章須要20分鐘,這篇文章主要講解了瀏覽器中引擎的渲染機制。javascript

DOMtree       ----|css

         |---->  RenderTreehtml

CSSOMtree  ----|html5

 

這是 JavaScript 工做原理的第十一章。java

迄今爲止,以前的 JavaScript 工做原理系列文章集中於關注 JavaScript 語言自己的功能,在瀏覽器中的執行狀況,如何優化等等。node

然而,當在構建網絡應用的時候,不只僅只是編寫本身運行的 JavaScript 代碼。所編寫的 JavaScript 代碼與運行環境息息相關。理解 JavaScript 運行環境,它的運行原理以及其組成會讓你構建出更好的應用而且一旦讓應用程序運行於各類環境下的時候,讓你更加成竹在胸地應對潛在的問題。git

 

 

那麼,讓咱們一探瀏覽器主要組件吧:github

  • 用戶界面: 包括地址欄,後退和前進按鈕,書籤菜單等等。本質上,這裏包含了除顯示用戶所看到的網頁自己的窗口之外的瀏覽器的每一個部分。
  • 瀏覽器引擎: 處理用戶界面和渲染引擎的交互
  • 渲染引擎: 負責顯示網頁。渲染引擎解析 HTML 和 CSS 並在屏幕上顯示解析的內容。
  • 網絡: 使用各個平臺的不一樣實現所發起的諸如 XHR 請求的網絡調用,這些網絡調用是基於跨平臺的接口實現的。
  • UI 後端: 負責繪製諸如複選框和窗口的核心部件。它暴露出一個平臺無關的泛型接口。它底層使用操做系統 UI 方法。
  • JavaScript 引擎: 咱們在以前的系列文章中有詳細介紹過。基本上,這是 JavaScript 代碼執行的地方。
  • 數據存儲: 網絡應用可能須要本地存儲全部數據。支持的存儲機制類型包括 localStorageindexDBWebSQL 以及 FileSystem

本文將專一介紹渲染引擎,由於它是用來處理 HTML 和 CSS 的解析和可視化的,而這些是大多數的 JavaScript 應用須要持續進行交互的方面。web

渲染引擎概述

渲染引擎的主要職責即在瀏覽器屏幕上顯示請求的頁面。後端

渲染引擎能夠顯示 HTML,XML 文檔以及圖片。若是使用額外的插件,就能夠顯示諸如 PDF 的不一樣類型的文檔。

渲染引擎

與 JavaScript 引擎相似,不一樣瀏覽器也使用不一樣的渲染引擎。如下爲比較流行的引擎:

  • Gecko-Firefox
  • WebKit-Safari
  • Blink-Chrome, Opera(從版本 15 開始)

渲染過程

渲染引擎從網絡層獲取到請求的文檔內容。

構建 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 樹相似這樣:

 

基本上,每一個元素是直接包含於其內的元素的父節點。而後依次類推。

構建 CSSOM 樹

CSSOM 即 CSS Object Model。當瀏覽器構建頁面的 DOM 樹的時候,它在 head 標籤部分遇到一個引用外部 theme.css 樣式表的 link 標籤。表示它可能須要樣式表來渲染頁面,因而便立刻分派一個請求來獲取樣式表。假設如下爲 theme.css 文件內容:



與 HTML 同樣,渲染引擎須要把 CSS 轉化爲瀏覽器能夠操做的東西-即 CSSOM。如下爲 CSSOM 的大概模樣:body { font-size: 16px; } p { font-weight: bold; } span { color: red; } p span { display: none; } img { float: right; }

 

想知道爲何 CSSOM 是樹狀結構的嗎?當爲頁面上的任意對象計算其最終的樣式集的時候,瀏覽器先把最爲通用的樣式規則應用於該節點(好比,它是 body 的子節點,會先應用 body 的全部樣式)而後經過應用更爲具體的樣式規則來遞歸重定義計算的樣式。

讓咱們看下具體的例子吧。body 中的 span 標籤中的任何文字樣式爲字體大小 16 像素且字體顏色爲紅色。這些樣式繼承自 body 元素。p 元素的子元素 span 因爲應用了更爲具體的樣式從而不會顯示其內容(display:none)。

還有,請注意以上 CSSOM 樹並不完整並且只顯示了樣式表中指定的重寫樣式。每一個瀏覽器提供了一份默認的樣式集即 『用戶代理樣式』- 這即當沒有提供任何樣式的時候的默認顯示樣式。咱們的樣式只是簡單地重寫了這些默認樣式。

 

構建渲染樹

HTML 中的可視化指令和 CSSOM 樹的樣式數據結合起來建立渲染樹。

你可能爲問渲染樹是什麼?它是按順序構建可視化元素並顯示在屏幕上的樹。它是帶有相應的樣式的 HTML 的視覺表現。該樹旨在按正確的順序繪製內容。

在 Webkit 中渲染樹中的每一個節點便是一個渲染器或者渲染器對象。

如下爲以上的 DOM 和 CSSOM 樹合成的渲染器樹的大概模樣:

爲了建立渲染樹,瀏覽器大概作了幾下幾件事:

  • 從 DOM 樹的根節點開始,遍歷每一個可見節點。一些節點是不可見的(好比,script 標籤,meta 標籤等等),而後會被忽略,由於它們並不會在渲染的輸出中顯示。一些節點經過樣式隱藏而後也會被忽略。好比以上例子中的 span 節點,由於爲其顯式設置了 display: none 的樣式。
  • 瀏覽器爲每一個可見節點應用相對應的 CSSOM 規則並應用這些樣式規則。
  • 釋放出包含內容及其通過計算的樣式的可見節點。

能夠瀏覽下 RenderObject 的源碼(Webkit 中):https://github.com/WebKit/webkit/blob/fde57e46b1f8d7dde4b2006aaf7ebe5a09a6984b/Source/WebCore/rendering/RenderObject.h

看一下這個類的一些核心構件吧:

class RenderObject : public CachedImageClient {
  // 重繪整個對象。當邊框顏色改變或者邊框樣式更改的時候調用。
  
  Node* node() const { ... }
  
  RenderStyle* style;  // 計算的樣式
  const RenderStyle& style() const;
  
  ...
}

每一個渲染器對象表明一個矩形區域一般是和一個節點的 CSS 盒模型相對應。它包括諸如寬度,高度以及定位的幾何信息。

渲染樹佈局

當建立了渲染器而且添加到渲染樹的時候,它並無定位和大小的信息。計算這些值即稱爲佈局。

HTML 使用了流式佈局模型,意即大多數狀況下能夠一次性計算出渲染器的幾何信息。座標系統是相對於根渲染器的。這裏使用 Top 和 left 座標。

佈局是一個遞歸的過程-它從根渲染器開始進行渲染,根渲染器即 HTML 文檔的 html 元素。佈局繼續經過一部分或者整個渲染器層級結構遞歸進行,爲每一個須要計算幾何信息的渲染器計算其信息。

根渲染器的定位爲 0,0 和大小即爲瀏覽器窗口的可視化部分(好比 viewport)。

進行佈局的過程即計算出每一個節點在屏幕上顯示的準確位置。

繪製渲染樹

該階段,遍歷渲染器樹而後調用渲染器的 paint() 方法來在屏幕上顯示其內容。

繪製能夠是全局或增量式的(相似於佈局):

  • 全局-重繪整個樹。
  • 增量-以某種方式只更改部分渲染器而不會影響到整顆樹。渲染器做廢其在屏幕上的矩形區域。這會致使操做系統把它當作是一個須要重繪的區域並生成一個 paint 事件。操做系統會智能地把幾個區域合併成一個以提高渲染性能。

總之,理解繪製是個漸進式的過程是很重要的。爲了更好的交互體驗,渲染引擎會試圖儘快在屏幕上顯示內容。它不會等待全部的 HTML 結構解析完成纔開始構建和佈局渲染樹。會優先解析和顯示部份內容,與此同時持續處理從網絡接收的剩下的內容項。

腳本和樣式的處理順序

當解析器遇到 <script> 標籤的時候會當即解析和執行該標籤裏面的代碼。整個文檔的解析會中止直到腳本執行完畢。意即該過程是同步的。

當 script 引用的是一個外部資源,必須首先獲取該資源(也是同步的)。全部的解析會中止直到獲取該腳本資源。

HTML5 添加了一個選項來異步加載該資源,這樣就可使用另外的線程來解析和執行該資源。IE 可使用 defer屬性,其它可使用 async 屬性。IE10 如下使用 defer 屬性,IE10 以上也可使用 async 屬性。

這裏有一個須要注意的地方即 IE10 如下對於 defer 的支持,打開 https://caniuse.com 查找便可發現對於 IE10 如下的支持是一些須要注意的地方即 defer 的腳本有可能會在 DOMContentLoaded 事件以後纔開始運行,參見這裏,這裏就不作試驗了,有興趣能夠點擊這裏測試下 IE 下的表現。

這裏稍微作一下引伸,在 jQuery 源碼中,ready.js 有一段以下的代碼:

// Catch cases where $(document).ready() is called
// after the browser event has already occurred.
// Support: IE <=9 - 10 only
// Older IE sometimes signals "interactive" too soon
if ( document.readyState === "complete" ||
	( document.readyState !== "loading" && !document.documentElement.doScroll ) ) {

	// Handle it asynchronously to allow scripts the opportunity to delay ready
	window.setTimeout( jQuery.ready );

} else {

	// Use the handy event callback
	document.addEventListener( "DOMContentLoaded", completed );

	// A fallback to window.onload, that will always work
	window.addEventListener( "load", completed );
}

裏面的 window.setTimeout( jQuery.ready ); 是容許腳本有機會延遲執行 ready 事件。大概是爲 IE script 標籤的 defer 屬性準備的吧?

 

優化渲染性能

若想要優化網絡應用的性能,須要關注五個主要的方面。這些方面是你能夠進行控制的:

  1. JavaScript-以前的文章中有介紹了編寫不阻塞 UI ,高效的代碼等等。談到渲染時候,須要考慮 JavaScript 代碼是如何和頁面上的 DOM 元素進行交互的。JavaScript 會在界面上作不少的更改,特別是在單頁應用中。
  2. 樣式計算-這個過程即應用樣式規則到匹配選擇器的元素上。一旦定義了樣式規則,它們會應用於對應的元素,而後計算每一個元素的最終樣式。
  3. 佈局-一旦瀏覽器瞭解元素應用的樣式規則,它會開始計算元素所佔用的空間和其在瀏覽器屏幕上的顯示位置。根據網頁的佈局模型定義一個元素的佈局會影響到其它的元素。好比,<body> 標籤的寬度會影響到其子孫元素的寬度等等。這即意味着佈局過程是至關耗時的。繪製是在多個圖層完成的。
  4. 繪製-該階段即開始填充實際像素。這一過程包括繪製文字,顏色,圖片,邊框,陰影等全部每一個元素的可見部分。
  5. 合成-由於頁面部分被繪製成潛在的多層,它們必須在屏幕以正確的順序進行繪製,這樣頁面渲染纔會正常。這是相當重要的,特別是對於那些重疊元素。

優化 JavaScript 代碼

JavaScript 常常會在瀏覽器端觸發視覺改變。尤爲是在構建 SPA 的過程當中會更多。

這裏有一些優化 JavaScript 中部分代碼來提高渲染效率的建議:

  • 避免使用 setTimeout 或者 setInterval 來進行視覺的更改。這些會在幀的某個時間點調用 callback,有多是在幀的末尾。這樣就會形成卡頓。必須在幀的開始觸發視覺更改。

  • 把耗時的 JavaScript 移入以前提到的網頁工做線程

  • 使用微任務來處理跨多個幀的 DOM 更改。這是爲了預防當任務須要訪問 DOM,而網絡工做線程沒法辦到的狀況的。

    意即須要把一個大型的任務分割爲多個小任務而後根據不一樣的任務性質在 requestAnimationFrame ,setTimeout 或 setInterval 中執行。

優化樣式

經過添加和移除元素及更改屬性等等修改 DOM 會致使瀏覽器從新計算元素樣式及大多數狀況整個頁面或者部分頁面的佈局。

使用如下方法來優化渲染:

  • 減小選擇器的複雜度。選擇器複雜度會佔用超過計算所需元素樣式的 50% 的時間,剩餘時間即構建樣式自己。
  • 減小必須產生樣式計算的元素的個數。本質上,直接更改少數元素的樣式而不是使整個頁面的樣式失效。

優化佈局

佈局是很耗費瀏覽器性能的。考慮如下優化方案:

  • 儘量減小布局的數量。當更改樣式的時候,瀏覽器檢查樣式更改是否須要從新計算佈局。通常而言更改諸如 width, height, left, top 等和幾何學相關的屬性會須要佈局。因此,儘量避免修改它們。
  • 儘量使用 flexbox 來進行佈局而不是老式的佈局模型。它會渲染得更快而且會極大地提高網絡應用的性能。
  • 避免強制同步佈局。須要記住的是當運行 JavaScript的時候,上一幀的老的佈局值是已知的且能夠被查詢獲得。當訪問 box.offsetHeight 這並不會形成性能問題。然而,若是在訪問它以前更改它的樣式(好比爲元素動態添加樣式類),瀏覽器將不得不首先應用樣式更改而後運行佈局計算樣式。這將會很是耗時和耗資源,因此盡力避免這樣作。

優化繪製

這常常會是全部任務中最耗時的,因此儘可能避免觸發繪製。優化方案:

  • 更改除 transfroms 或者 opacity 外的屬性會觸發繪製。因此省着點用啊。
  • 當觸發一個佈局也會觸發繪製,由於更改元素的幾何信息會更改元素的展現效果。
  • 經過提高層和動畫編排來減小繪製區域。

擴展

參考谷歌官方關於性能的文檔,提高元素使用以下的代碼:

.moving-element {
  will-change: transform;
}

使用 FASTDOM 來避免強制同步佈局和抖動。

另外關於 JavaScript 代碼的優化方面,避免去處理一些微優化,好比使用 offsetTop 比用 getBoundingClientRect 速度更快,但這得基於所建立的網絡應用而言,假設建立一個遊戲,對性能要求很是高並且調用這些方法的地方多,那麼性能的提高將會很可觀的。還記得之前常常會去使用諸如 jsperf 來測試某個方法的速度,千萬別鑽牛角尖,因地制宜,避免掉入去計較那些微小的優化而付出過大的精力。

關於渲染可使用一些骨架圖來提高用戶體驗。

 

一些想法

  • 關於性能的體驗,其實你能夠想象成造房子吧,假如是整個翻修固然是會更加耗時,可是若是裝修某個區域就會提高性能。
  • 而後有其中的某個屬性會提高性能,這可能理解爲『工欲善其事必先利其器』。
  • 關於任務的切分能夠理解爲,建設設計的哲學,小技巧。

參考資源