瀏覽器的渲染原理簡介
看到這個(gè)標(biāo)題大家一定會(huì)想到這篇神文《How Browsers Work》,這篇文章把瀏覽器的很多細(xì)節(jié)講得很細(xì),而且也被翻譯成了中文。為什么我還想寫一篇呢?因?yàn)閮蓚€(gè)原因,
1)這篇文章太長了,閱讀成本太大,不能一口氣讀完。
2)花了大力氣讀了這篇文章后可以了解很多,但似乎對(duì)工作沒什么幫助。
所以,我準(zhǔn)備寫下這篇文章來解決上述兩個(gè)問題。希望你能在上班途中,或是坐馬桶時(shí)就能讀完,并能從中學(xué)會(huì)一些能用在工作上的東西。
瀏覽器工作大流程
廢話少說,先來看個(gè)圖:
從上面這個(gè)圖中,我們可以看到那么幾個(gè)事:
1)瀏覽器會(huì)解析三個(gè)東西:
- 一個(gè)是HTML/SVG/XHTML,事實(shí)上,Webkit有三個(gè)C++的類對(duì)應(yīng)這三類文檔。解析這三種文件會(huì)產(chǎn)生一個(gè)DOM Tree。
- CSS,解析CSS會(huì)產(chǎn)生CSS規(guī)則樹。
- Javascript,腳本,主要是通過DOM API和CSSOM API來操作DOM Tree和CSS Rule Tree.
2)解析完成后,瀏覽器引擎會(huì)通過DOM Tree 和 CSS Rule Tree 來構(gòu)造 Rendering Tree。注意:
- Rendering Tree 渲染樹并不等同于DOM樹,因?yàn)橐恍┫馠eader或display:none的東西就沒必要放在渲染樹中了。
- CSS 的 Rule Tree主要是為了完成匹配并把CSS Rule附加上Rendering Tree上的每個(gè)Element。也就是DOM結(jié)點(diǎn)。也就是所謂的Frame。
- 然后,計(jì)算每個(gè)Frame(也就是每個(gè)Element)的位置,這又叫l(wèi)ayout和reflow過程。
3)最后通過調(diào)用操作系統(tǒng)Native GUI的API繪制。
DOM解析
HTML的DOM Tree解析如下:
- <html>
- <html>
- <head>
- <title>Web page parsing</title>
- </head>
- <body>
- <p>
- <h1>Web page parsing</h1>
- <p>This is an example Web page.</p>
- </p>
- </body>
- </html>
上面這段HTML會(huì)解析成這樣:
下面是另一個(gè)有SVG標(biāo)簽的情況。
#p#
CSS解析
CSS的解析大概是下面這個(gè)樣子(下面主要說的是Gecko也就是Firefox的玩法),假設(shè)我們有下面的HTML文檔:
- <doc>
- <title>A few quotes</title>
- <para>
- Franklin said that <quote>"A penny saved is a penny earned."</quote>
- </para>
- <para>
- FDR said <quote>"We have nothing to fear but <span>fear itself.</span>"</quote>
- </para>
- </doc>
于是DOM Tree是這個(gè)樣子:
然后我們的CSS文檔是這樣的:
- /* rule 1 */ doc { display: block; text-indent: 1em; }
- /* rule 2 */ title { display: block; font-size: 3em; }
- /* rule 3 */ para { display: block; }
- /* rule 4 */ [class="emph"] { font-style: italic; }
于是我們的CSS Rule Tree會(huì)是這個(gè)樣子:
注意,圖中的第4條規(guī)則出現(xiàn)了兩次,一次是獨(dú)立的,一次是在規(guī)則3的子結(jié)點(diǎn)。所以,我們可以知道,建立CSS Rule Tree是需要比照著DOM Tree來的。CSS匹配DOM Tree主要是從右到左解析CSS的Selector,好多人以為這個(gè)事會(huì)比較快,其實(shí)并不一定。關(guān)鍵還看我們的CSS的Selector怎么寫了。
注意:CSS匹配HTML元素是一個(gè)相當(dāng)復(fù)雜和有性能問題的事情。所以,你就會(huì)在N多地方看到很多人都告訴你,DOM樹要小,CSS盡量用id和class,千萬不要過渡層疊下去,……
通過這兩個(gè)樹,我們可以得到一個(gè)叫Style Context Tree,也就是下面這樣(把CSS Rule結(jié)點(diǎn)Attach到DOM Tree上):
所以,F(xiàn)irefox基本上來說是通過CSS 解析 生成 CSS Rule Tree,然后,通過比對(duì)DOM生成Style Context Tree,然后Firefox通過把Style Context Tree和其Render Tree(Frame Tree)關(guān)聯(lián)上,就完成了。注意:Render Tree會(huì)把一些不可見的結(jié)點(diǎn)去除掉。而Firefox中所謂的Frame就是一個(gè)DOM結(jié)點(diǎn),不要被其名字所迷惑了。
注:Webkit不像Firefox要用兩個(gè)樹來干這個(gè),Webkit也有Style對(duì)象,它直接把這個(gè)Style對(duì)象存在了相應(yīng)的DOM結(jié)點(diǎn)上了。#p#
渲染
渲染的流程基本上如下(黃色的四個(gè)步驟):
- 計(jì)算CSS樣式
- 構(gòu)建Render Tree
- Layout – 定位坐標(biāo)和大小,是否換行,各種position, overflow, z-index屬性 ……
- 正式開畫
注意:上圖流程中有很多連接線,這表示了Javascript動(dòng)態(tài)修改了DOM屬性或是CSS屬會(huì)導(dǎo)致重新Layout,有些改變不會(huì),就是那些指到天上的箭頭,比如,修改后的CSS rule沒有被匹配到,等。
這里重要要說兩個(gè)概念,一個(gè)是Reflow,另一個(gè)是Repaint。這兩個(gè)不是一回事。
- Repaint——屏幕的一部分要重畫,比如某個(gè)CSS的背景色變了。但是元素的幾何尺寸沒有變。
- Reflow——意味著元件的幾何尺寸變了,我們需要重新驗(yàn)證并計(jì)算Render Tree。是Render Tree的一部分或全部發(fā)生了變化。這就是Reflow,或是Layout。(HTML使用的是flow based layout,也就是流式布局,所以,如果某元件的幾何尺寸發(fā)生了變化,需要重新布局,也就叫reflow)reflow 會(huì)從<html>這個(gè)root frame開始遞歸往下,依次計(jì)算所有的結(jié)點(diǎn)幾何尺寸和位置,在reflow過程中,可能會(huì)增加一些frame,比如一個(gè)文本字符串必需被包裝起來。
下面是一個(gè)打開Wikipedia時(shí)的Layout/reflow的視頻(注:HTML在初始化的時(shí)候也會(huì)做一次reflow,叫intial reflow),你可以感受一下:
Reflow的成本比Repaint的成本高得多的多。DOM Tree里的每個(gè)結(jié)點(diǎn)都會(huì)有reflow方法,一個(gè)結(jié)點(diǎn)的reflow很有可能導(dǎo)致子結(jié)點(diǎn),甚至父點(diǎn)以及同級(jí)結(jié)點(diǎn)的reflow。在一些高性能的電腦上也許還沒什么,但是如果reflow發(fā)生在手機(jī)上,那么這個(gè)過程是非常痛苦和耗電的。
所以,下面這些動(dòng)作有很大可能會(huì)是成本比較高的。
- 當(dāng)你增加、刪除、修改DOM結(jié)點(diǎn)時(shí),會(huì)導(dǎo)致Reflow或Repaint
- 當(dāng)你移動(dòng)DOM的位置,或是搞個(gè)動(dòng)畫的時(shí)候。
- 當(dāng)你修改CSS樣式的時(shí)候。
- 當(dāng)你Resize窗口的時(shí)候(移動(dòng)端沒有這個(gè)問題),或是滾動(dòng)的時(shí)候。
- 當(dāng)你修改網(wǎng)頁的默認(rèn)字體時(shí)。
注:display:none會(huì)觸發(fā)reflow,而visibility:hidden只會(huì)觸發(fā)repaint,因?yàn)闆]有發(fā)現(xiàn)位置變化。
多說兩句關(guān)于滾屏的事,通常來說,如果在滾屏的時(shí)候,我們的頁面上的所有的像素都會(huì)跟著滾動(dòng),那么性能上沒什么問題,因?yàn)槲覀兊娘@卡對(duì)于這種把全屏像素往上往下移的算法是很快。但是如果你有一個(gè)fixed的背景圖,或是有些Element不跟著滾動(dòng),有些Elment是動(dòng)畫,那么這個(gè)滾動(dòng)的動(dòng)作對(duì)于瀏覽器來說會(huì)是相當(dāng)相當(dāng)痛苦的一個(gè)過程。你可以看到很多這樣的網(wǎng)頁在滾動(dòng)的時(shí)候性能有多差。因?yàn)闈L屏也有可能會(huì)造成reflow。
基本上來說,reflow有如下的幾個(gè)原因:
- Initial。網(wǎng)頁初始化的時(shí)候。
- Incremental。一些Javascript在操作DOM Tree時(shí)。
- Resize。其些元件的尺寸變了。
- StyleChange。如果CSS的屬性發(fā)生變化了。
- Dirty。幾個(gè)Incremental的reflow發(fā)生在同一個(gè)frame的子樹上。
好了,我們來看一個(gè)示例吧:
- 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!'));
當(dāng)然,我們的瀏覽器是聰明的,它不會(huì)像上面那樣,你每改一次樣式,它就reflow或repaint一次。一般來說,瀏覽器會(huì)把這樣的操作積攢一批,然后做一次reflow,這又叫異步reflow或增量異步reflow。但是有些情況瀏覽器是不會(huì)這么做的,比如:resize窗口,改變了頁面默認(rèn)的字體,等。對(duì)于這些操作,瀏覽器會(huì)馬上進(jìn)行reflow。
但是有些時(shí)候,我們的腳本會(huì)阻止瀏覽器這么干,比如:如果我們請(qǐng)求下面的一些DOM值:
- offsetTop, offsetLeft, offsetWidth, offsetHeight
- scrollTop/Left/Width/Height
- clientTop/Left/Width/Height
- IE中的 getComputedStyle(), 或 currentStyle
因?yàn)椋绻覀兊某绦蛐枰@些值,那么瀏覽器需要返回最新的值,而這樣一樣會(huì)flush出去一些樣式的改變,從而造成頻繁的reflow/repaint。#p#
減少reflow/repaint
下面是一些Best Practices:
1)不要一條一條地修改DOM的樣式。與其這樣,還不如預(yù)先定義好css的class,然后修改DOM的className。
- // bad
- var left = 10,
- top = 10;
- el.style.left = left + "px";
- el.style.top = top + "px";
- // Good
- el.className += " theclassname";
- // Good
- el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
2)把DOM離線后修改。如:
- 使用documentFragment 對(duì)象在內(nèi)存里操作DOM
- 先把DOM給display:none(有一次reflow),然后你想怎么改就怎么改。比如修改100次,然后再把他顯示出來。
- clone一個(gè)DOM結(jié)點(diǎn)到內(nèi)存里,然后想怎么改就怎么改,改完后,和在線的那個(gè)的交換一下。
3)不要把DOM結(jié)點(diǎn)的屬性值放在一個(gè)循環(huán)里當(dāng)成循環(huán)里的變量。不然這會(huì)導(dǎo)致大量地讀寫這個(gè)結(jié)點(diǎn)的屬性。
4)盡可能的修改層級(jí)比較低的DOM。當(dāng)然,改變層級(jí)比較底的DOM有可能會(huì)造成大面積的reflow,但是也可能影響范圍很小。
5)為動(dòng)畫的HTML元件使用fixed或absoult的position,那么修改他們的CSS是不會(huì)reflow的。
6)千萬不要使用table布局。因?yàn)榭赡芎苄〉囊粋€(gè)小改動(dòng)會(huì)造成整個(gè)table的重新布局。
In this manner, the user agent can begin to lay out the table once the entire first row has been received. Cells in subsequent rows do not affect column widths. Any cell that has content that overflows uses the ‘overflow’ property to determine whether to clip the overflow content. This algorithm may be inefficient since it requires the user agent to have access to all the content in the table before determining the final layout and may demand more than one pass.
幾個(gè)工具和幾篇文章
有時(shí)候,你會(huì)也許會(huì)發(fā)現(xiàn)在IE下,你不知道你修改了什么東西,結(jié)果CPU一下子就上去了到100%,然后過了好幾秒鐘repaint/reflow才完成,這種事情以IE的年代時(shí)經(jīng)常發(fā)生。所以,我們需要一些工具幫我們看看我們的代碼里有沒有什么不合適的東西。
- Chrome下,Google的SpeedTracer是個(gè)非常強(qiáng)悍的工作讓你看看你的瀏覽渲染的成本有多大。其實(shí)Safari和Chrome都可以使用開發(fā)者工具里的一個(gè)Timeline的東東。
- Firefox下這個(gè)基于Firebug的叫Firebug Paint Events的插件也不錯(cuò)。
- IE下你可以用一個(gè)叫dynaTrace的IE擴(kuò)展。
最后,別忘了下面這幾篇提高瀏覽器性能的文章:
- Google – Web Performance Best Practices
- Yahoo – Best Practices for Speeding Up Your Web Site
- Steve Souders – 14 Rules for Faster-Loading Web Sites
參考
- David Baron的演講:Fast CSS: How Browsers Lay Out Web Pages:slideshow, all slides, audio (MP3), Session page, Lanyrd page
- How Browsers Work: http://taligarsiel.com/Projects/howbrowserswork1.htm
- Mozilla 的 Style System Overview:https://developer.mozilla.org/en-US/docs/Style_System_Overview
- Mozilla 的 Note of reflow: http://www-archive.mozilla.org/newlayout/doc/reflow.html
- Rendering: repaint, reflow/relayout, restyle:http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/
- Effective Rendering CSS:http://css-tricks.com/efficiently-rendering-css/
- Webkit Rendering文檔:http://trac.webkit.org/wiki/WebCoreRendering