瀏覽器的渲染原理簡介

作者: 陳皓  發布時間: 2013-07-27 14:07  閱讀: 18438 次  推薦: 39   原文鏈接   [收藏]  

  看到這個標題大家一定會想到這篇神文《How Browsers Work》,這篇文章把瀏覽器的很多細節講得很細,而且也被翻譯成了中文。為什么我還想寫一篇呢?因為兩個原因,

  1)這篇文章太長了,閱讀成本太大,不能一口氣讀完。

  2)花了大力氣讀了這篇文章后可以了解很多,但似乎對工作沒什么幫助。

  所以,我準備寫下這篇文章來解決上述兩個問題。希望你能在上班途中,或是坐馬桶時就能讀完,并能從中學會一些能用在工作上的東西。

  瀏覽器工作大流程

  廢話少說,先來看個圖:

  從上面這個圖中,我們可以看到那么幾個事:

  1)瀏覽器會解析三個東西:

  • 一個是 HTML/SVG/XHTML,事實上,Webkit 有三個 C++ 的類對應這三類文檔。解析這三種文件會產生一個 DOM Tree。
  • CSS,解析 CSS 會產生 CSS 規則樹。
  • Javascript,腳本,主要是通過 DOM API 和 CSSOM API 來操作 DOM Tree 和 CSS Rule Tree.

  2)解析完成后,瀏覽器引擎會通過 DOM Tree 和 CSS Rule Tree 來構造 Rendering Tree。注意:

  • Rendering Tree 渲染樹并不等同于 DOM 樹,因為一些像 Header 或 display:none 的東西就沒必要放在渲染樹中了。
  • CSS 的 Rule Tree 主要是為了完成匹配并把 CSS Rule 附加上 Rendering Tree 上的每個 Element。也就是 DOM 結點。也就是所謂的 Frame。
  • 然后,計算每個 Frame(也就是每個 Element)的位置,這又叫 layout 和 reflow 過程。

  3)最后通過調用操作系統 Native GUI 的 API 繪制。

  DOM 解析

  HTML 的 DOM Tree 解析如下:

<html>
<head>
  <title>Web page parsing</title>
</head>
<body>
  <div>
  <h1>Web page parsing</h1>
  <p>This is an example Web page.</p>
  </div>
</body>
</html>

  上面這段 HTML 會解析成這樣:

  下面是另一個有 SVG 標簽的情況。

  CSS 解析

  CSS 的解析大概是下面這個樣子(下面主要說的是 Gecko 也就是 Firefox 的玩法),假設我們有下面的 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 是這個樣子:

  然后我們的 CSS 文檔是這樣的:

/* rule 1 */ doc { display: block; text-indent: 1em; }
/* rule 2 */ title { display: block; font-size: 3em; }
/* rule 3 */ para { display: block; }
/* rule 4 */ [] { font-style: italic; }

  于是我們的 CSS Rule Tree 會是這個樣子:

  注意,圖中的第 4 條規則出現了兩次,一次是獨立的,一次是在規則 3 的子結點。所以,我們可以知道,建立 CSS Rule Tree 是需要比照著 DOM Tree 來的。CSS 匹配 DOM Tree 主要是從右到左解析 CSS 的 Selector,好多人以為這個事會比較快,其實并不一定。關鍵還看我們的 CSS 的 Selector 怎么寫了。

  注意:CSS 匹配 HTML 元素是一個相當復雜和有性能問題的事情。所以,你就會在N多地方看到很多人都告訴你,DOM 樹要小,CSS 盡量用 id 和 class,千萬不要過渡層疊下去,……

  通過這兩個樹,我們可以得到一個叫 Style Context Tree,也就是下面這樣(把 CSS Rule 結點 Attach 到 DOM Tree 上):

  所以,Firefox 基本上來說是通過 CSS 解析生成 CSS Rule Tree,然后,通過比對 DOM 生成 Style Context Tree,然后 Firefox 通過把 Style Context Tree 和其 Render Tree(Frame Tree)關聯上,就完成了。注意:Render Tree 會把一些不可見的結點去除掉。而 Firefox 中所謂的 Frame 就是一個 DOM 結點,不要被其名字所迷惑了

  注:Webkit 不像 Firefox 要用兩個樹來干這個,Webkit 也有 Style 對象,它直接把這個 Style 對象存在了相應的 DOM 結點上了。

  渲染

  渲染的流程基本上如下(黃色的四個步驟):

  1. 計算 CSS 樣式
  2. 構建 Render Tree
  3. Layout – 定位坐標和大小,是否換行,各種 position, overflow, z-index 屬性 ……
  4. 正式開畫

  注意:上圖流程中有很多連接線,這表示了 Javascript 動態修改了 DOM 屬性或是 CSS 屬性會導致重新 Layout,有些改變不會,就是那些指到天上的箭頭,比如,修改后的 CSS rule 沒有被匹配到,等。

  這里重要要說兩個概念,一個是 Reflow,另一個是 Repaint。這兩個不是一回事。

  • Repaint——屏幕的一部分要重畫,比如某個 CSS 的背景色變了。但是元素的幾何尺寸沒有變。
  • Reflow——意味著元件的幾何尺寸變了,我們需要重新驗證并計算 Render Tree。是 Render Tree 的一部分或全部發生了變化。這就是 Reflow,或是 Layout。(HTML 使用的是 flow based layout,也就是流式布局,所以,如果某元件的幾何尺寸發生了變化,需要重新布局,也就叫 reflow)reflow 會從 <html> 這個 root frame 開始遞歸往下,依次計算所有的結點幾何尺寸和位置,在 reflow 過程中,可能會增加一些 frame,比如一個文本字符串必需被包裝起來。

  下面是一個打開 Wikipedia 時的 Layout/reflow 的視頻(注:HTML 在初始化的時候也會做一次 reflow,叫 intial reflow),你可以感受一下:

  Reflow 的成本比 Repaint 的成本高得多的多。DOM Tree 里的每個結點都會有 reflow 方法,一個結點的 reflow 很有可能導致子結點,甚至父點以及同級結點的 reflow。在一些高性能的電腦上也許還沒什么,但是如果 reflow 發生在手機上,那么這個過程是非常痛苦和耗電的。

  所以,下面這些動作有很大可能會是成本比較高的。

  • 當你增加、刪除、修改 DOM 結點時,會導致 Reflow 或 Repaint。
  • 當你移動 DOM 的位置,或是搞個動畫的時候。
  • 當你修改 CSS 樣式的時候。
  • 當你 Resize 窗口的時候(移動端沒有這個問題),或是滾動的時候。
  • 當你修改網頁的默認字體時。

  注:display:none 會觸發 reflow,而 visibility:hidden 只會觸發 repaint,因為沒有發現位置變化。

  多說兩句關于滾屏的事,通常來說,如果在滾屏的時候,我們的頁面上的所有的像素都會跟著滾動,那么性能上沒什么問題,因為我們的顯卡對于這種把全屏像素往上往下移的算法是很快。但是如果你有一個 fixed 的背景圖,或是有些 Element 不跟著滾動,有些 Elment 是動畫,那么這個滾動的動作對于瀏覽器來說會是相當相當痛苦的一個過程。你可以看到很多這樣的網頁在滾動的時候性能有多差。因為滾屏也有可能會造成 reflow。

  基本上來說,reflow 有如下的幾個原因:

  • Initial。網頁初始化的時候。
  • Incremental。一些 Javascript 在操作 DOM Tree 時。
  • Resize。其些元件的尺寸變了。
  • StyleChange。如果 CSS 的屬性發生變化了。
  • Dirty。幾個 Incremental 的 reflow 發生在同一個 frame 的子樹上。

  好了,我們來看一個示例吧:

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 一次。一般來說,瀏覽器會把這樣的操作積攢一批,然后做一次 reflow,這又叫異步 reflow 或增量異步 reflow。但是有些情況瀏覽器是不會這么做的,比如:resize 窗口,改變了頁面默認的字體,等。對于這些操作,瀏覽器會馬上進行 reflow。

  但是有些時候,我們的腳本會阻止瀏覽器這么干,比如:如果我們請求下面的一些 DOM 值:

  1. offsetTop, offsetLeft, offsetWidth, offsetHeight
  2. scrollTop/Left/Width/Height
  3. clientTop/Left/Width/Height
  4. IE 中的 getComputedStyle (), 或 currentStyle

  因為,如果我們的程序需要這些值,那么瀏覽器需要返回最新的值,而這樣一樣會 flush 出去一些樣式的改變,從而造成頻繁的 reflow/repaint。

  減少 reflow/repaint

  下面是一些 Best Practices:

  1)不要一條一條地修改 DOM 的樣式。與其這樣,還不如預先定義好 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 對象在內存里操作 DOM。
  • 先把 DOM 給 display:none (有一次 repaint),然后你想怎么改就怎么改。比如修改 100 次,然后再把他顯示出來。
  • clone 一個 DOM 結點到內存里,然后想怎么改就怎么改,改完后,和在線的那個的交換一下。

  3)不要把 DOM 結點的屬性值放在一個循環里當成循環里的變量。不然這會導致大量地讀寫這個結點的屬性。

  4)盡可能的修改層級比較低的 DOM。當然,改變層級比較底的 DOM 有可能會造成大面積的 reflow,但是也可能影響范圍很小。

  5)為動畫的 HTML 元件使用 fixed 或 absoult 的 position,那么修改他們的 CSS 是不會 reflow 的。

  6)千萬不要使用 table 布局。因為可能很小的一個小改動會造成整個 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.

Fixed layout, CSS 2.1 Specification

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.

Automatic layout, CSS 2.1 Specification

  幾個工具和幾篇文章

  有時候,你會也許會發現在 IE 下,你不知道你修改了什么東西,結果 CPU 一下子就上去了到 100%,然后過了好幾秒鐘 repaint/reflow 才完成,這種事情以 IE 的年代時經常發生。所以,我們需要一些工具幫我們看看我們的代碼里有沒有什么不合適的東西。

  • Chrome 下,Google 的 SpeedTracer 是個非常強悍的工作讓你看看你的瀏覽渲染的成本有多大。其實 Safari 和 Chrome 都可以使用開發者工具里的一個 Timeline 的東東。
  • Firefox 下這個基于 Firebug 的叫 Firebug Paint Events 的插件也不錯。
  • IE 下你可以用一個叫 dynaTrace 的 IE 擴展。

  最后,別忘了下面這幾篇提高瀏覽器性能的文章:

  參考

39
0
 
標簽:瀏覽器
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()