
瀏覽器的渲染過程
首先,我們先來了解一下瀏覽器的渲染過程是什么樣的,也就是說瀏覽器把一堆代碼呈現到頁面上的過程是什么樣子的,瀏覽器采用流式布局模型(Flow Bsaed Layout),根據下圖,我們可以總結出瀏覽器的渲染步驟為:

步驟:
1.解析HTML代碼,生成DOM樹(DOM Tree);解析CSS代碼,生成CSSOM樹(CSS Tree);
2.將DOM樹和CSSOM樹進行結合從而構建起渲染樹(Render Tree);
Render Tree類似于DOM Tree,但存在很大的區別:Render Tree能夠識別樣式,Render Tree中的每個節點都有自己的樣式,而且Render Tree不包含隱藏的節點,比如display:none的節點,因為這些節點不會用于頁面呈現。
3.回流(Layout),根據生成的Render Tree,進行Layout,得到節點的位置、大小;
4.重繪(Painting),根據Render Tree以及回流得到的位置信息,確定各節點的絕對位置,得到各節點的絕對像素;
5.呈現(Display),將像素發送給GPU,展示到頁面上。
由于瀏覽器采用流式布局,對Render Tree的計算通常只需要遍歷一次就可以完成,但是table以及其內部元素除外,他們可能需要多次計算,通常要花三倍同等元素的時間,這也是為什么要避免使用table布局的原因。
————分割線————
在步驟2中,渲染樹(Render Tree)是如果構建的呢?

如上圖所示,總結出的構建步驟為:
1.從DOM樹的根節點開始遍歷每個可見節點;
遍歷的是每個可見節點,那么不可見的節點包括:
(a) 一些不會渲染輸出的節點,比如script、meta、link等;
(b) 一些通過css進行隱藏的節點。比如display:none。注意,利用visibility和opacity隱藏的節點,還是會顯示在渲染樹上的。只有display:none的節點才不會顯示在渲染樹上。
2.對于每個可見的節點,找到CSSOM樹中對應的規則,并應用它們;
3.根據每個可見節點以及其對應的樣式,組合生成渲染樹。
又是怎么組合的呢?
簡單是就是一個匹配的過程,要將每個HTML元素節點與之正確的樣式相匹配。因為節點位置屬性將通過CSS選擇器鏈的優先級來決定,渲染樹中的某個結點可能會同時滿足多個選擇器鏈,這時候就要通過選擇器的優先級來完成屬性的賦值。
這時候,將渲染結點同時滿足的幾個選擇器鏈通過其優先級加權算值,從小到大依次覆蓋渲染結點;而如何確定此渲染結點是否滿足某個選擇器鏈呢?這也是一個逐層判斷的過程:從此渲染結點開始,判斷此結點是否與選擇器鏈表的當前選擇器相匹配。如果匹配,判斷此選擇器與下一個選擇器的關系:如果為NONE,表示本選擇器是選擇器鏈的最后一個,返回成功;如果關系為AND (比如:#id.class),選擇下一個選擇器與本渲染結點繼續比較;如果關系為CHILD,表示本選擇器是下一個選擇器的子結點,返回下一個選擇器與下一個渲染結點的匹配結果;否則,關系為DESCENDANT,選擇器和渲染結點各指向下一個結點,然后將渲染結點繼續回溯,直到第一個滿足回溯后的選擇器的結點,此時將繼續判斷回溯后的選擇器和回溯后的渲染結點是否匹配。
————分割線————
重繪
什么是重繪呢?當Render Tree中的一些元素需要更新屬性,而這些屬性只是影響元素的外觀、風格,而不會影響布局的,比如改變背景顏色等,這就會引起瀏覽器重繪(Painting)。重繪有一定的代價,因為瀏覽器會驗證DOM樹上其他元素的可見性。例如:
某網站首頁頁面中,將藍色框內導航欄的背景顏色變為粉色,其他的不變,并沒有改變整體布局和各個部分的位置,所以此時會引起重繪,不會引起回流。

回流
當Render Tree中的部分節點因為元素的尺寸、布局、隱藏等改變而需要重新構建,這就會引起瀏覽器回流(reflow)。每個頁面至少需要一次回流,就是在頁面第一次加載的時候(瀏覽器渲染過程步驟3),因為要第一次構建Render Tree。在回流的時候,瀏覽器會使渲染樹中受到影響的部分失效,并重新構造這部分渲染樹,完成回流后,瀏覽器會重新繪制受影響的部分到屏幕中,即重繪。例如:
某網站首頁頁面中,將藍色框內導航欄直接刪掉,則下面的所有部分會進行上移,整體的布局發生了變化,所以此時會引起回流,接著進行重繪。
在這里引出了回流與重繪的一個最大的區別:
【 回流一定會引起重繪,重繪不一定會引起回流 】回流是影響瀏覽器性能的關鍵因素,因為其變化會涉及到部分頁面甚至整個頁面的布局更新。
那么,何時會觸發回流重繪呢?
(a) 添加或刪除可見的DOM元素;
(b) 元素的位置發生變化;
(c) 元素的尺寸發生變化(包括外邊距、內邊框、邊框大小、高度和寬度等);
(d) 內容發生變化,比如文本變化或圖片被另一個不同尺寸的圖片所替代;
(e) 頁面一開始渲染的時候(這肯定避免不了);
(f) 瀏覽器的窗口尺寸變化(因為回流是根據視口的大小來計算元素的位置和大小的)......
————分割線————
優化
上面提到,回流重繪給瀏覽器性能帶來了很大的影響,為了提高性能,可以采取下面的一些措施進行性能優化。
1、瀏覽器優化
現代瀏覽器大多是通過隊列機制來批量更新布局,瀏覽器把修改操作放在一個隊列里面,然后再批量執行,如果是60HZ刷新率的話,至少要16.6ms才會清空隊列。但是當你獲取布局信息的操作的時候,會導致隊列被強制清空,觸發回流重繪以確保返回正確的值,主要包括以下屬性和方法:
- offsetTop、offsetLeft、offsetWidth、offsetHeight
- scrollTop、scrollLeft、scrollWidth、scrollHeight
- clientTop、clientLeft、clientWidth、clientHeight
- width height
- getComputedStyle()
- getBoundingClientRect()
所以,上述的屬性和方法要避免頻繁使用,最好不用,如果要用,可以想辦法把值緩存下來。
2、減少回流與重繪
CSS
- 使用 transform 代替 top ;
- 使用 visibility 代替 dispaly。因為visibility只會引起重繪,而dispaly會引起回流;
- 避免 table 布局。可能一個小小的改動會導致整個table的重新布局;
- 盡可能的在DOM樹的最末端改變class;
- 避免設置多層的內聯樣式,CSS選擇器從右向左匹配查找,避免節點層級太多;
- 將動畫效果應用到position屬性為absolute或者fixed的元素上,避免影響其他元素布局,這樣只有重繪,同時,控制動畫速度可以用requestAnimationFrame;
- 避免使用CSS表達式;
- CSS3硬件加速(GPU加速)
......
JS
- 避免頻繁的操作樣式,最好一次性重寫style屬性,或者將樣式列表定義為class并一次性更改class屬性;
- 避免頻繁的操作DOM;
- 避免頻繁讀取會引發回流重繪的屬性;
- 對有復雜動畫的元素使用絕對定位