原文請查閱這裏,略有刪減,本文采用知識共享署名 4.0 國際許可協議共享,BY Troland。javascript
本系列持續更新中,Github 地址請查閱這裏。css
這是 JavaScript 工做原理的第十一章。html
迄今爲止,以前的 JavaScript 工做原理系列文章集中於關注 JavaScript 語言自己的功能,在瀏覽器中的執行狀況,如何優化等等。html5
然而,當在構建網絡應用的時候,不只僅只是編寫本身運行的 JavaScript 代碼。所編寫的 JavaScript 代碼與運行環境息息相關。理解 JavaScript 運行環境,它的運行原理以及其組成會讓你構建出更好的應用而且一旦讓應用程序運行於各類環境下的時候,讓你更加成竹在胸地應對潛在的問題。java
那麼,讓咱們一探瀏覽器主要組件吧:node
本文將專一介紹渲染引擎,由於它是用來處理 HTML 和 CSS 的解析和可視化的,而這些是大多數的 JavaScript 應用須要持續進行交互的方面。git
渲染引擎的主要職責即在瀏覽器屏幕上顯示請求的頁面。github
渲染引擎能夠顯示 HTML,XML 文檔以及圖片。若是使用額外的插件,就能夠顯示諸如 PDF 的不一樣類型的文檔。web
與 JavaScript 引擎相似,不一樣瀏覽器也使用不一樣的渲染引擎。如下爲比較流行的引擎:後端
渲染引擎從網絡層獲取到請求的文檔內容。
渲染引擎的第一步即解析 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 即 CSS Object Model。當瀏覽器構建頁面的 DOM 樹的時候,它在 head
標籤部分遇到一個引用外部 theme.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 的大概模樣:
想知道爲何 CSSOM 是樹狀結構的嗎?當爲頁面上的任意對象計算其最終的樣式集的時候,瀏覽器先把最爲通用的樣式規則應用於該節點(好比,它是 body 的子節點,會先應用 body 的全部樣式)而後經過應用更爲具體的樣式規則來遞歸重定義計算的樣式。
讓咱們看下具體的例子吧。body
中的 span
標籤中的任何文字樣式爲字體大小 16 像素且字體顏色爲紅色。這些樣式繼承自 body
元素。p
元素的子元素 span
因爲應用了更爲具體的樣式從而不會顯示其內容(display:none
)。
還有,請注意以上 CSSOM 樹並不完整並且只顯示了樣式表中指定的重寫樣式。每一個瀏覽器提供了一份默認的樣式集即 『用戶代理樣式』- 這即當沒有提供任何樣式的時候的默認顯示樣式。咱們的樣式只是簡單地重寫了這些默認樣式。
HTML 中的可視化指令和 CSSOM 樹的樣式數據結合起來建立渲染樹。
你可能爲問渲染樹是什麼?它是按順序構建可視化元素並顯示在屏幕上的樹。它是帶有相應的樣式的 HTML 的視覺表現。該樹旨在按正確的順序繪製內容。
在 Webkit 中渲染樹中的每一個節點便是一個渲染器或者渲染器對象。
如下爲以上的 DOM 和 CSSOM 樹合成的渲染器樹的大概模樣:
爲了建立渲染樹,瀏覽器大概作了幾下幾件事:
display: none
的樣式。能夠瀏覽下 RenderObject 的源碼(Webkit 中):github.com/WebKit/webk…
看一下這個類的一些核心構件吧:
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 屬性準備的吧?
若想要優化網絡應用的性能,須要關注五個主要的方面。這些方面是你能夠進行控制的:
<body>
標籤的寬度會影響到其子孫元素的寬度等等。這即意味着佈局過程是至關耗時的。繪製是在多個圖層完成的。JavaScript 常常會在瀏覽器端觸發視覺改變。尤爲是在構建 SPA 的過程當中會更多。
這裏有一些優化 JavaScript 中部分代碼來提高渲染效率的建議:
避免使用 setTimeout
或者 setInterval
來進行視覺的更改。這些會在幀的某個時間點調用 callback
,有多是在幀的末尾。這樣就會形成卡頓。必須在幀的開始觸發視覺更改。
把耗時的 JavaScript 移入以前提到的網頁工做線程。
使用微任務來處理跨多個幀的 DOM 更改。這是爲了預防當任務須要訪問 DOM,而網絡工做線程沒法辦到的狀況的。
意即須要把一個大型的任務分割爲多個小任務而後根據不一樣的任務性質在 requestAnimationFrame
,setTimeout
或 setInterval
中執行。
經過添加和移除元素及更改屬性等等修改 DOM 會致使瀏覽器從新計算元素樣式及大多數狀況整個頁面或者部分頁面的佈局。
使用如下方法來優化渲染:
佈局是很耗費瀏覽器性能的。考慮如下優化方案:
flexbox
來進行佈局而不是老式的佈局模型。它會渲染得更快而且會極大地提高網絡應用的性能。box.offsetHeight
這並不會形成性能問題。然而,若是在訪問它以前更改它的樣式(好比爲元素動態添加樣式類),瀏覽器將不得不首先應用樣式更改而後運行佈局計算樣式。這將會很是耗時和耗資源,因此盡力避免這樣作。這常常會是全部任務中最耗時的,因此儘可能避免觸發繪製。優化方案:
參考谷歌官方關於性能的文檔,提高元素使用以下的代碼:
.moving-element {
will-change: transform;
}
複製代碼
使用 FASTDOM 來避免強制同步佈局和抖動。
另外關於 JavaScript 代碼的優化方面,避免去處理一些微優化,好比使用 offsetTop
比用 getBoundingClientRect
速度更快,但這得基於所建立的網絡應用而言,假設建立一個遊戲,對性能要求很是高並且調用這些方法的地方多,那麼性能的提高將會很可觀的。還記得之前常常會去使用諸如 jsperf 來測試某個方法的速度,千萬別鑽牛角尖,因地制宜,避免掉入去計較那些微小的優化而付出過大的精力。
關於渲染可使用一些骨架圖來提高用戶體驗。
今日頭條招人啦!發送簡歷到 likun.liyuk@bytedance.com ,便可走快速內推通道,長期有效!國際化PGC部門的JD以下:c.xiumi.us/board/v5/2H…,也可內推其餘部門!
本系列持續更新中,Github 地址請查閱這裏。