瀏覽器的渲染機制
一、一些概念:
1.DOM:文檔對象模型(Document Object Model),瀏覽器將HTML解析成樹形的數據結構,簡稱DOM
(1)DOM是什麼
DOM就是一個編程接口,就是一套API。
DOM是針對HTML文檔、XML等文檔的一套API。就類似於JDBC是針對數據庫的一套API一樣。
(2)DOM的用途
DOM 是用來訪問或操作HTML文檔、XHTML文檔、XML文檔中的節點元素。
(3)DOM與其他技術的聯繫
JavaScript 可以通過 DOM 來訪問和操作HTML文檔所有的元素。
JavaScript是一種腳本語言,通常通過DOM來獲得和操作HTML屬性。
(4)DOM Tree是指通過DOM將HTML頁面進行解析,並生成的HTML tree樹狀結構和對應訪問方法
(5)Render Tree(渲染樹):DOM 和 CSSOM 合併後生成 Render Tree
2.CSSOM:CSS Object Model,瀏覽器將CSS代碼解析成樹形的數據結構。
DOM 和 CSSOM 都是以 Bytes → characters → tokens → nodes → object model
二、瀏覽器的渲染:
1.瀏覽器會解析三個東西:
(1)HTML/SVG/XHTML:Webkit 有三個 C++ 的類對應這三類文檔。解析這三種文件會產生一個 DOM Tree;
(2)CSS:解析 CSS 會產生 CSS 規則樹;
(3)Javascript:腳本,通過 DOM API 和 CSSOM API 來操作 DOM Tree 和 CSS Rule Tree.
2.解析完成後,瀏覽器引擎會通過 DOM Tree 和 CSS Rule Tree 來構造 Rendering Tree;
3.DOM 樹的構建過程是一個深度遍歷過程:當前節點的所有子節點都構建好後纔會去構建當前節點的下一個兄弟節點。
4.Render Tree 和DOM一樣,以多叉樹的形式保存了每個節點的css屬性、節點本身屬性、以及節點的孩子節點。
5.CSS Rule 結點 Attach 到 DOM Tree 上,我們可以得到一個叫 Style Context Tree(樣式上下文樹),Firefox 基本上來說是通過 CSS 解析生成 CSS Rule Tree,然後,通過比對 DOM 生成 Style Context Tree,然後 Firefox 通過把 Style Context Tree 和其 Render Tree(Frame Tree)關聯上,就完成了,(注:Webkit 不像 Firefox 要用兩個樹來幹這個,Webkit 也有 Style 對象,它直接把這個 Style 對象存在了相應的 DOM 結點上了)
注意:
1.display:none 的節點不會被加入 Render Tree,而 visibility: hidden 則會,所以,如果某個節點最開始是不顯示的,設爲 display:none 是更優的;
2.建立 CSS Rule Tree 是需要比照着 DOM Tree 來的。CSS 匹配 DOM Tree 主要是從右到左解析 CSS 的 Selector,CSS 匹配 HTML 元素是一個相當複雜和有性能問題的事情。 DOM Tree 要小,CSS 儘量用 id 和 class,千萬不要過渡層疊下去。
3.Render Tree 會把一些不可見的結點去除掉( display:none)
三、瀏覽器的渲染過程(webkit):
1.計算 CSS 樣式:
Create/Update DOM And request css/image/js:瀏覽器請求到HTML代碼後,在生成DOM的最開始階段(應該是 Bytes → characters 後),並行發起css、圖片、js的請求,無論他們是否在HEAD裏。
注意:發起 js 文件的下載 request 並不需要 DOM 處理到那個 script 節點,比如:簡單的正則匹配就能做到這一點,雖然實際上並不一定是通過正則:)。
2.構建 Render Tree:
(1)Create/Update Render CSSOM:CSS文件下載完成,開始構建CSSOM。
(2)Create/Update Render Tree:所有CSS文件下載完成,CSSOM構建結束後,和 DOM 一起生成 Render Tree。
3.Layout:計算出每個節點在屏幕中的位置。
有了Render Tree,瀏覽器已經能知道網頁中有哪些節點、各個節點的CSS定義以及他們的從屬關係。定位座標和大小,是否換行,各種 position, overflow, z-index 屬性 ……
4.正式開畫(Painting):
Layout後,瀏覽器已經知道了哪些節點要顯示(which nodes are visible)、每個節點的CSS屬性是什麼(their computed styles)、每個節點在屏幕中的位置是哪裏(geometry)。就進入了最後一步:Painting,按照算出來的規則,通過顯卡,把內容畫到屏幕上。
注意:
1.以上四個步驟前2個步驟之所有使用 “Create/Update” 是因爲DOM、CSSOM、Render Tree都可能在第一次Painting後又被更新多次,比如JS修改了DOM或者CSS屬性。
2.兩個重要概念:(重繪重排)
(1)Repaint——屏幕的一部分要重畫,比如某個 CSS 的背景色變了。但是元素的幾何尺寸沒有變
Reflow——意味着元件的幾何尺寸變了,我們需要重新驗證並計算 Render Tree。是 Render Tree 的一部分或全部發生了變化。
(Reflow 的成本比 Repaint 的成本高得多的多)
Layout 和 Painting 也會被重複執行,除了DOM、CSSOM更新的原因外,圖片下載完成後也需要調用Layout 和 Painting來更新網頁。
DOM Tree 裏的每個結點都會有 reflow 方法,一個結點的 reflow 很有可能導致子結點,甚至父點以及同級結點的 reflow
(2)下面這些動作有很大可能會是成本比較高的:
a.增加、刪除、修改 DOM 結點時,會導致 Reflow 或 Repaint。
b.移動 DOM 的位置,或是搞個動畫的時候。
c.修改 CSS 樣式的時候。
d. Resize 窗口的時候(移動端沒有這個問題),或是滾動的時候。
e.修改網頁的默認字體時。
注:display:none 會觸發 reflow,而 visibility:hidden 只會觸發 repaint,因爲沒有發現位置變化。
(3)reflow 有如下的幾個原因:
a.Initial。網頁初始化的時候。
b.Incremental。一些 Javascript 在操作 DOM Tree 時。
c.Resize。其些元件的尺寸變了。
d.StyleChange。如果 CSS 的屬性發生變化了。
e.Dirty。幾個 Incremental 的 reflow 發生在同一個 frame 的子樹上。
(4)減少 reflow/repaint:
a.不要一條一條地修改 DOM 的樣式。與其這樣,還不如預先定義好 css 的 class,然後修改 DOM 的 className。
b.把 DOM 離線後修改。如:
*使用 documentFragment 對象在內存裏操作 DOM。
*先把 DOM 給 display:none (有一次 repaint),然後你想怎麼改就怎麼改。比如修改 100 次,然後 再把他顯示出來。
*clone 一個 DOM 結點到內存裏,然後想怎麼改就怎麼改,改完後,和在線的那個的交換一下
c.不要把 DOM 結點的屬性值放在一個循環裏當成循環裏的變量。不然這會導致大量地讀寫這個結點的屬性。
d.儘可能的修改層級比較低的 DOM。當然,改變層級比較底的 DOM 有可能會造成大面積的 reflow,但是也可能影響範圍很小。
e.爲動畫的 HTML 元件使用 fixed 或 absoult 的 position,那麼修改他們的 CSS 是不會 reflow 的。
f.千萬不要使用 table 佈局。因爲可能很小的一個小改動會造成整個 table 的重新佈局。
四、面試可能的問題:
script標籤的位置會影響首屏時間麼(爲什麼script要放在body底部)?
答案:不影響(如果這裏裏的首屏指的是頁面從白板變成網頁畫面——也就是第一次Painting),但有可能截斷首屏的內容,使其只顯示上面一部分。
爲什麼說是“有可能”呢?,如果該js下載地比css還快,或者script標籤不在第一屏的html裏,實際上是不影響的。明白這一影響邊界非常重要,這樣我們在考察頁面性能瓶頸的時候就有的放矢了。舉個例子:在網頁的第二屏有一個通用模塊,實際上我們是可以把它的js邏輯獨立成一個文件,將模塊的html和js標籤放在一起做成獨立的模板引進來的。
問題的總結補充:
1.如果script標籤的位置不在首屏範圍內,不影響首屏時間(首屏時間和DomContentLoad事件沒有必然的先後關係);
2.所有CSS儘早加載是減少首屏時間的最關鍵;
2.所有的script標籤應該放在body底部是很有道理的;
3.script標籤放在body底部,做與不做async(異步)或者defer(推遲)處理,都不會影響首屏時間,但影響DomContentLoad和load的時間,進而影響依賴他們的代碼的執行的開始時間。
從性能最優的角度考慮,即使在body底部的script標籤也會拖慢首屏出來的速度,因爲瀏覽器在最一開始就會請求它對應的js文件,而這,佔用了有限的TCP鏈接數、帶寬甚至運行它所需要的CPU。這也是爲什麼script標籤會有async或defer屬性的原因之一。
可是,在複雜的實際應用場景中,要貫徹這幾條結論可能會遇到問題,比如:
你的頁面是分模塊來寫的,每一個模塊都有自己的html、js甚至css,當把這些模塊湊到一個頁面中的時候就會出現js自然而然地出現在HTML中間部分。你很難把script標籤都放到底部
即使你把script標籤都放到底部,但script標籤的存在終究是拖慢了首屏時間、DomContendLoad和loaded的時間。如果只有一個script標籤,我們可以加一個async,但多個async的script標籤的結果會是js文件被亂序執行的,這顯然不是我們想要的。所以:
這時候,如果有個組件,幫助我根據優先級的不同,在特定的時間下載特定的資源,同時需要保證腳本的執行順序,就能完美的解決這個問題,因此出現:
Tiny-Loader 組件:它與一般資源加載器不同的是,它可以保證資源下載以後的執行順序,可以按需進行資源加載。
在前端性能優化過程中,發現許多js,css並不是頁面一開始就需要的,而是在用戶某個操作以後,才需要執行/渲染出來的。將那些js、css緩加載,可以大大減小頁面的首屏時間,減少頁面出load事件的負擔.
使用方法:
Loader.async(['xxxx.css', 'yyyy.js'])
eg:
<html>
<head></head>
<body>
<div class="container">container</div>
<script>
Loader.sync(['xxxx.js', 'yyyy.js', 'zzzz.js'])
</script>
</body>
</html>
把各自模塊需要的js文件放在對應的html裏,然後通過Tiny-loader引用,就可以保證每個模塊文件裏有各自的html和js,模塊間互不影響,js也會在頁面最後纔會下載,不會堵塞住頁面的渲染.