首先,你應該了解的就是,瀏覽器是如何渲染一個頁面的。
先看一個大致的流程圖

它的總體流程是這樣的:
1)瀏覽器解析這三個東西:
- 解析HTML/XHTML/SVG,生成DOM樹(事實上,Webkit有三個C++的類對應這三類文檔以用於解析)。
- 解析css文件產生CSS Rule樹(css規則樹)。
- 解析javascript,通過DOM API和CSSOM API來操作DOM樹和CSS Rule樹。
2)解析完成后,瀏覽器會根據DOM樹和CSS Rule樹來構造渲染樹(Rendering Tree)。
- 渲染樹並不完全等同於DOM樹,因為一些display:none的東西就沒必要放在渲染樹中了。
- CSS Rule樹主要是為了完成匹配並把CSS Rule附加到渲染樹上的每個DOM結點。
- 然后,計算每個DOM節點的位置,這又叫layout和reflow過程。
3)最后通過調用操作系統Native GUI的API繪制(painting)。
拋去其中的細節,再簡單一點的說法就是:DOM樹解析->css解析->渲染(也就是構建渲染樹以及最終呈現到瀏覽器上的過程)
這里 主要針對第三步的渲染過程進行一下講解:
- 計算css樣式,這一步對應着總體流程中的這句話,CSS Rule樹主要是為了完成匹配並把CSS Rule附加到渲染樹上的每個DOM結點,也就是說,最終的渲染樹映射了 DOM 的結構。在渲染樹中,每一個文本字符串都被當做一個獨立的 renderer。每個渲染對象都包含了與之對應的計算過樣式的DOM 對象(或者一個文本塊)。換句話說,渲染樹描述了 DOM 的直觀的表現形式。
- 構建Render Tree。
- Layout – 定位坐標和大小,是否換行,各種position, overflow, z-index屬性 ……
- 正式開畫
需要注意的是:Javascript如果動態修改了DOM屬性或是CSS屬會導致重新Layout(Reflow),當然有些屬性改變不會。
這里,這里重要要說兩個概念,一個是Reflow,另一個是Repaint
Repaint(重繪)
當在頁面上修改了一些不需要改變定位的樣式的時候(比如background-color,border-color,visibility),瀏覽器只會將新的樣式重新繪制給元素(這就叫一次“重繪”或者“重新定 義樣式”)。這時只需要屏幕的一部分要重畫。
Reflow(重排)
當頁面上的改變影響了文檔內容、結構或者元素定位時,就會發生重排(或稱“重新布局”)。重排通常由以下改變觸發:
- DOM 操作(如元素增、刪、改或者改變元素順序)
- 內容的改變,包括 Form 表單中文字的變化
- 計算或改變 CSS 屬性
- 增加或刪除一個樣式表
- 瀏覽器窗口的操作(改變大小、滾動窗口)
- 激活偽類(如:hover狀態)
這時,我們需要重新驗證並計算Render Tree。是Render Tree的一部分或全部發生了變化。這就是Reflow,或是Layout。(HTML使用的是flow based layout,也就是流式 布局,所以,如果某元件的幾何尺寸發生了變化,需要重新布局,也就叫reflow)reflow 會從<html>這個root frame開始遞歸往下,依次計算所有的結點幾何尺寸和位置,在 reflow過程中,可能會增加一些frame,比如一個文本字符串必需被包裝起來。
可以看出,這兩個動作對於瀏覽器的性能都有較大的影響,當然reflow的成本比repaint的成本高好多。那么,瀏覽器又是如何避免成本增加,從而優化渲染的呢?
瀏覽器如何優化渲染?
1、瀏覽器盡最大努力限制重排的過程僅覆蓋已更改的元素的區域。舉個例子,一個 position 為 absolue 或 fixed 的元素的大小變化只影響它自身和子孫元素,而對一個 position 為 static 的元素做同樣的操作就會引起所有它后面元素的重排。
2、當運行一段Jjavascript 代碼的時候,瀏覽器會將一些修改緩存起來,然后當代碼執行的時候,一次性的將這些修改執行。舉例來說,這段代碼會觸發一次重繪和一次重排:
var bstyle = document.body.style; // cache
bstyle.padding = "20px"; // reflow, repaint
bstyle.border = "10px solid red"; // 再一次的 reflow 和 repaint
bstyle.color = "blue"; // repaint
bstyle.backgroundColor = "#fad"; // repaint
bstyle.fontSize = "2em"; // reflow, repaint
// new DOM element - reflow, repaint
document.body.appendChild(document.createTextNode('dude!'));
瀏覽器不會像上面那樣,你每改一次樣式,它就reflow或repaint一次。一般來說,瀏覽器會把這樣的(都是設置style屬性,而不涉及其他類似讀取屬性的操作)操作積攢一批,然后做一次reflow,這又叫異步reflow或增量異步reflow。但是有些情況瀏覽器是不會這么做的,比如:resize窗口,改變了頁面默認的字體,等。對於這些操作,瀏覽器會馬上進行reflow。
但是有些時候,我們的腳本會阻止瀏覽器這么干,比如:如果我們請求下面的一些DOM值:(比如我們在上面的例子中若加一個讀取屬性的操作則會引起又一次的重排)
- offsetTop, offsetLeft, offsetWidth, offsetHeight
- scrollTop/Left/Width/Height
- clientTop/Left/Width/Height
- IE中的 getComputedStyle(), 或 currentStyle
因為,如果我們的程序需要這些值,那么瀏覽器需要返回最新的值,而這樣一樣會flush出去一些樣式的改變,從而造成頻繁的reflow/repaint。
當然,我們可以通過改變書寫習慣而做一些認為的性能優化:
實際優化建議
- 創建合法的 HTML 和 CSS ,別忘了制定文件編碼,Style 應該寫在 head 標簽中,script 標簽應該加載 body 標簽結束的位置
- 試着簡化和優化 CSS 選擇器(這個優化點被大多數使用 CSS 預處理器的開發者忽略了)。將嵌套層數控制在最小。
- 在你的腳本中,盡可能的減少 DOM 的操作。把所有東西都緩存起來,包括屬性和對象(如果它可被重復使用)。進行復雜的操作的時候,最好操作一個“離線”的元素(“離線”元素的意思是與 DOM 對象分開、僅存在內存中的元素),然后將這個元素插入到 DOM 中。
例如:
1、使用documentFragment 對象在內存里操作DOM,類似以下的代碼示例:
// Create the fragment
var fragment = document.createDocumentFragment();
//add DOM to fragment
for(var i = 0; i < 10; i++) {
var spanNode = document.createElement("span");
spanNode.innerHTML = "number:" + i;
fragment.appendChild(spanNode);
}
//add this DOM to body
document.body.appendChild(spanNode);
2、先把DOM給display:none(有一次reflow),然后你想怎么改就怎么改。比如修改100次,然后再把他顯示出來。
3、clone一個DOM結點到內存里,然后想怎么改就怎么改,改完后,和在線的那個的交換一下
- 不要一條一條地修改DOM的樣式。與其這樣,還不如預先定義好css的class,然后修改DOM的className
- 盡可能的只對 position 為 absolute 或 fix 的元素做動畫
- 當滾動時禁用一些復雜的
:hover動畫是一個很好的主意(例如,給 body 標簽加一個 no-hover 的 class - 千萬不要使用table布局。因為可能很小的一個小改動會造成整個table的重新布局
