瀏覽器工做原理簡介

1、瀏覽器主要構成css

  1. 用戶界面 - 包括地址欄、後退/前進按鈕、書籤目錄等,也就是你所看到的除了用來顯示你所請求頁面的主窗口以外的其餘部分。html

  2. 瀏覽器引擎 - 用來查詢及操做渲染引擎的接口。html5

  3. 渲染引擎 - 用來顯示請求的內容,例如,若是請求內容爲html,它負責解析html及css,並將解析後的結果顯示出來。node

  4. 網絡 - 用來完成網絡調用,例如http請求,它具備平臺無關的接口,能夠在不一樣平臺上工做。web

  5. UI後端 - 用來繪製相似組合選擇框及對話框等基本組件,具備不特定於某個平臺的通用接口,底層使用操做系統的用戶接口。算法

  6. JS解釋器 - 用來解釋執行JS代碼。後端

  7. 數據存儲 - 屬於持久層,瀏覽器須要在硬盤中保存相似cookie的各類數據,HTML5定義了web database技術,這是一種輕量級完整的客戶端存儲技術瀏覽器

 

2、渲染引擎緩存

  本文所討論的瀏覽器——Firefox、Chrome和Safari是基於兩種渲染引擎構建的,Firefox使用Geoko——Mozilla自主研發的渲染引擎,Safari和Chrome都使用webkit。cookie

  Webkit是一款開源渲染引擎,它原本是爲Linux平臺研發的,後來由Apple移植到Mac及Windows上,相關內容請參考http://webkit.org

  渲染引擎首先經過網絡得到所請求文檔的內容,一般以8K分塊的方式完成。

  下面是渲染引擎在取得內容以後的基本流程:

  解析html以構建dom樹 -> 構建render樹 -> 佈局render樹 -> 繪製render樹

  渲染引擎開始解析html,並將標籤轉化爲內容樹中的dom節點。接着,它解析外部CSS文件及style標籤中的樣式信息。這些樣式信息以及html中的可見性指令將被用來構建另外一棵樹——render樹。

  Render樹由一些包含有顏色和大小等屬性的矩形組成,它們將被按照正確的順序顯示到屏幕上。

  Render樹構建好了以後,將會執行佈局過程,它將肯定每一個節點在屏幕上的確切座標。再下一步就是繪製,即遍歷render樹,並使用UI後端層繪製每一個節點。

  值得注意的是,這個過程是逐步完成的,爲了更好的用戶體驗,渲染引擎將會盡量早的將內容呈現到屏幕上,並不會等到全部的html都解析完成以後再去構建和佈局render樹。它是解析完一部份內容就顯示一部份內容,同時,可能還在經過網絡下載其他內容。

webkit:

Gecko:

圖4:Mozilla的Geoko渲染引擎主流程

  從圖3和4中能夠看出,儘管webkit和Gecko使用的術語稍有不一樣,他們的主要流程基本相同。Gecko稱可見的格式化元素組成的樹爲frame樹,每一個元素都是一個frame,webkit則使用render樹這個名詞來命名由渲染對象組成的樹。Webkit中元素的定位稱爲佈局,而Gecko中稱爲迴流。Webkit稱利用dom節點及樣式信息去構建render樹的過程爲attachment,Gecko在html和dom樹之間附加了一層,這層稱爲內容接收器,至關製造dom元素的工廠。

3、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標籤的狀況。

 

4、CSS解析

Webkit CSS解析器(Webkit CSS parser)

  Webkit使用Flex和Bison解析生成器從CSS語法文件中自動生成解析器。回憶一下解析器的介紹,Bison建立一個自底向上的解析器,Firefox使用自頂向下解析器。它們都是將每一個css文件解析爲樣式表對象,每一個對象包含css規則,css規則對象包含選擇器和聲明對象,以及其餘一些符合css語法的對象。

  

處理腳本及樣式表的順序(The order of processing scripts and style sheets)

  腳本

  web的模式是同步的,開發者但願解析到一個script標籤時當即解析執行腳本,並阻塞文檔的解析直到腳本執行完。若是腳本是外引的,則網絡必須先請求到這個資源——這個過程也是同步的,會阻塞文檔的解析直到資源被請求到。這個模式保持了不少年,而且在html4及html5中都特別指定了。開發者能夠將腳本標識爲defer,以使其不阻塞文檔解析,並在文檔解析結束後執行。Html5增長了標記腳本爲異步的選項,以使腳本的解析執行使用另外一個線程。

  預解析(Speculative parsing)

  Webkit和Firefox都作了這個優化,當執行腳本時,另外一個線程解析剩下的文檔,並加載後面須要經過網絡加載的資源。這種方式可使資源並行加載從而使總體速度更快。須要注意的是,預解析並不改變Dom樹,它將這個工做留給主解析過程,本身只解析外部資源的引用,好比外部腳本、樣式表及圖片,也就是不會使外部腳本操做DOM樹。

  樣式表(Style sheets)

  樣式表採用另外一種不一樣的模式。理論上,既然樣式表不改變Dom樹,也就沒有必要停下文檔的解析等待它們,然而,存在一個問題,腳本可能在文檔的解析過程當中請求樣式信息,若是樣式尚未加載和解析,腳本將獲得錯誤的值,顯然這將會致使不少問題,這看起來是個邊緣狀況,但確實很常見。Firefox在存在樣式表還在加載和解析時阻塞全部的腳本,而Chrome只在當腳本試圖訪問某些可能被未加載的樣式表所影響的特定的樣式屬性時才阻塞這些腳本。

  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樹:

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:

注意,圖中的第4條規則出現了兩次,一次是獨立的,一次是在規則3的子結點。因此,咱們能夠知道,創建CSS Rule Tree是須要比照着DOM Tree來的。CSS匹配DOM Tree主要是從右到左解析CSS的Selector,好多人覺得這個事會比較快,其實並不必定。關鍵還看咱們的CSS的Selector怎麼寫了。

注意:CSS匹配HTML元素是一個至關複雜和有性能問題的事情。因此,DOM樹要小,CSS儘可能用id和class,千萬不要過渡層疊下去

將DOM樹和csss rule tree attach在一塊兒,獲得下面的Style Context 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結點上了。

 

5、渲染樹的構建

  當Dom樹構建完成時,瀏覽器開始構建另外一棵樹——渲染樹。渲染樹由元素顯示序列中的可見元素組成,它是文檔的可視化表示,構建這棵樹是爲了以正確的順序繪製文檔內容。

  Firefox將渲染樹中的元素稱爲frames,WebKit則用renderer或渲染對象來描述這些元素。

  一個渲染對象知道怎麼佈局及繪製本身及它的children。

  RenderObject是Webkit的渲染對象基類,它的定義以下:

class RenderObject{
virtual void layout();
virtual void paint(PaintInfo);
virtual void rect repaintRect();
Node* node;//the DOM node
RenderStyle* style;// the computed style
RenderLayer* containgLayer; //the containing z-index layer
}

  每一個渲染對象用一個和該節點的css盒模型相對應的矩形區域來表示,正如css2所描述的那樣,它包含諸如寬、高和位置之類的幾何信息。盒模型的類型受該節點相關的display樣式屬性的影響(參考樣式計算章節)。下面的webkit代碼說明了如何根據display屬性決定某個節點建立何種類型的渲染對象。

RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
Document* doc = node->document();
RenderArena* arena = doc->renderArena();
...
RenderObject* o = 0;
switch (style->display()) {
case NONE:
break;
case INLINE:
o = new (arena) RenderInline(node);
break;
case BLOCK:
o = new (arena) RenderBlock(node);
break;
case INLINE_BLOCK:
o = new (arena) RenderBlock(node);
break;
case LIST_ITEM:
o = new (arena) RenderListItem(node);
break;
...
}
return o;
}

  元素的類型也須要考慮,例如,表單控件和表格帶有特殊的框架。

  在Webkit中,若是一個元素想建立一個特殊的渲染對象,它須要重寫「createRenderer」方法,使渲染對象指向不包含幾何信息的樣式對象。

  渲染樹和Dom樹的關係(The render tree relation to the DOM tree)

  渲染對象和Dom元素相對應,但這種對應關係不是一對一的,不可見的Dom元素不會被插入渲染樹,例如head元素。另外,display屬性爲none的元素也不會在渲染樹中出現(visibility屬性爲hidden的元素將出如今渲染樹中)。

  還有一些Dom元素對應幾個可見對象,它們通常是一些具備複雜結構的元素,沒法用一個矩形來描述。例如,select元素有三個渲染對象——一個顯示區域、一個下拉列表及一個按鈕。一樣,當文本由於寬度不夠而折行時,新行將做爲額外的渲染元素被添加。另外一個多個渲染對象的例子是不規範的html,根據css規範,一個行內元素只能僅包含行內元素或僅包含塊狀元素,在存在混合內容時,將會建立匿名的塊狀渲染對象包裹住行內元素。

  一些渲染對象和所對應的Dom節點不在樹上相同的位置,例如,浮動和絕對定位的元素在文本流以外,在兩棵樹上的位置不一樣,渲染樹上標識出真實的結構,並用一個佔位結構標識出它們原來的位置。

 建立樹的流程(The flow of constructing the tree)

  Firefox中,表述爲一個監聽Dom更新的監聽器,將frame的建立委派給Frame Constructor,這個構建器計算樣式(參看樣式計算)並建立一個frame。

  Webkit中,計算樣式並生成渲染對象的過程稱爲attachment,每一個Dom節點有一個attach方法,attachment的過程是同步的,調用新節點的attach方法將節點插入到Dom樹中。

  處理html和body標籤將構建渲染樹的根,這個根渲染對象對應被css規範稱爲containing block的元素——包含了其餘全部塊元素的頂級塊元素。它的大小就是viewport——瀏覽器窗口的顯示區域,Firefox稱它爲viewPortFrame,webkit稱爲RenderView,這個就是文檔所指向的渲染對象,樹中其餘的部分都將做爲一個插入的Dom節點被建立。

 

6、佈局

 layout通常有下面這幾個部分:

  1. parent渲染對象決定它的寬度

  2. parent渲染對象讀取chilidren,並:

    a. 放置child渲染對象(設置它的x和y)

    b. 在須要時(它們當前爲dirty或是處於全局layout或者其餘緣由)調用child渲染對象的layout,這將計算child的高度

    c. parent渲染對象使用child渲染對象的累積高度,以及margin和padding的高度來設置本身的高度-這將被parent渲染對象的parent使用

    d. 將dirty標識設置爲false

  Firefox使用一個「state」對象(nsHTMLReflowState)作爲參數去佈局(firefox稱爲reflow),state包含parent的寬度及其餘內容。

  Firefox佈局的輸出是一個「metrics」對象(nsHTMLReflowMetrics)。它包括渲染對象計算出的高度。

  寬度計算

  渲染對象的寬度使用容器的寬度、渲染對象樣式中的寬度及margin、border進行計算。例如,下面這個div的寬度:

  <div />

  webkit中寬度的計算過程是(RenderBox類的calcWidth方法):

  • 容器的寬度是容器的可用寬度和0中的最大值,這裏的可用寬度爲:contentWidth=clientWidth()-paddingLeft()-paddingRight(),clientWidth和clientHeight表明一個對象內部的不包括border和滑動條的大小
  • 元素的寬度指樣式屬性width的值,它能夠經過計算容器的百分比獲得一個絕對值
  • 加上水平方向上的border和padding

  到這裏是最佳寬度的計算過程,如今計算寬度的最大值和最小值,若是最佳寬度大於最大寬度則使用最大寬度,若是小於最小寬度則使用最小寬度。最後緩存這個值,當須要layout但寬度未改變時使用。

  Line breaking

  當一個渲染對象在佈局過程當中須要折行時,則暫停並告訴它的parent它須要折行,parent將建立額外的渲染對象並調用它們的layout。

 

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

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

  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值:

  • offsetTop, offsetLeft, offsetWidth, offsetHeight
  • scrollTop/Left/Width/Height
  • clientTop/Left/Width/Height
  • 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結點的屬性值放在一個循環裏當成循環裏的變量。否則這會致使大量地讀寫這個結點的屬性。

  注:js中操做或者遍歷對象的屬性,也是很耗性能

4.儘量的修改層級比較低的DOM。固然,改變層級比較底的DOM有可能會形成大面積的reflow,可是也可能影響範圍很小。

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

  fixed或absoult的position脫離DOM樹

6.千萬不要使用table佈局。由於可能很小的一個小改動會形成整個table的從新佈局。

 

7、繪製

繪製順序

  css2定義了繪製過程的順序——http://www.w3.org/TR/CSS21/zindex.html。這個就是元素壓入堆棧的順序,這個順序影響着繪製,堆棧從後向前進行繪製。

  一個塊渲染對象的堆棧順序是:

  1. 背景色

  2. 背景圖

  3. border

  4. children

  5. outline

相關文章
相關標籤/搜索