瀏覽器渲染原理筆記 --《How Browser Work》讀後總結

綜述css

以前使用ExtJS時遇到一個問題:爲何依次設置多個組件的可見性界面會卡頓?在瞭解HTML的dom操做相關內容的時候也好奇這個東西究竟是怎麼回事,而後尤爲搞不懂CSS和Html分管樣式和網頁結構,這個東西是怎麼實現的,是否是很複雜?html

帶着這些問題,看了一些文章,尤爲是據說了Redraw和Reflow的概念以後,開始研究了dom的性能調優,最近看了一篇《how browser work》,以爲寫得很詳細,結合以前看的文章,解決了很多的困惑,寫一篇對這個文章的讀後總結,順便記下來本身掌握的一些瀏覽器性能的知識。web

瀏覽器的整體結構正則表達式

 瀏覽器主要部件包括:express

  1. 用戶界面:包括瀏覽器輸入欄、前進、後退、主頁等非網頁展現區。
  2. 瀏覽器引擎:用戶交互和呈現引擎之間的橋樑。
  3. 呈現引擎:實現dom和Css計算和渲染功能的部分,也是本文的主題。
  4. JS解釋器。parse和執行JS代碼。
  5. 數據存儲,數據存儲的持久層。

具體結構以下圖所示:編程

 

 

                            Figure : Browser components瀏覽器

 

呈現引擎(rendering engine)緩存

呈現引擎的工做就是呈現,包括呈現HTML/XML/pdf/image/CSS等等,固然咱們主要關心呈現HTML+CSS這兩個部分。網絡

呈現引擎這個名字咱們可能不熟,可是WebKit、Blink你們應該聽過,Safari的呈現引擎就是Webkit,Chrome目前的呈現引擎是Blink,是Webkit的一個分支,另外Firefox也有本身的呈現引擎Gecko,IE的是Trident(本文寫做的時候應該沒有Edge)。本文介紹呈現引擎主要圍繞Webkit和Trident來說,會涉及到二者的異同,也就是Chrome、Safari和Firefox。dom

呈現引擎基本工做流程

 

 

如上圖所示,呈現引擎從網絡中收到資源文件後,首先Parse HTML文件,生成Dom樹,而後開始parse外部和內部的CSS樣式,生成CSS規則組,而後以Dom樹爲基礎生成Render tree,render tree雖然沒有渲染在頁面上,但包含了足夠的信息render出一個像素頁面。所以調用render tree的layout方法開始根據dom tree結構,上面每一個元素的display/width/height/minwidth/minheight/maxwidth/maxheight/border/padding/margin/position/float/left/right/top/bottom等layout相關的屬性計算出對應元素的真實位置信息,在此基礎上調用render tree的paint方法依據位置排布按特定順序逐個繪製組件。

Webkit和firefox Gecko呈現引擎的工做流圖

 

Figure : WebKit main flow

 

 

 

 

Figure : Mozilla's Gecko rendering engine main flow 

能夠看出整體流程大同小異,更多的是名詞叫法的差別。

本文隨後將按照上面的整體流程,分爲parse(包括dom tree和style rules生成)、render tree(frame tree)、layout(reflow)&paint(draw)三個大章節介紹呈現引擎。

Parse: Dom tree and style rules

Parse術語淺析

對於一個操做,包括編程語言和方程、公式,做者首先以文本形式寫成,但要計算機理解並執行,就須要按照必定語法寫成,這樣計算機才能根據必定的原則,把文本轉化成結構化的操做樹,而後再根據這些操做來執行命令,從文本轉化爲操做樹的過程即Parse。

例如我輸入了2+3-1這段文本,將會返回以下parse tree:

 

 

具體來講,2+3-1能parse成結構化的操做,分紅了兩步,第一步是詞法分析,第二步是語法分析。

詞法分析

詞法分析就是根據這門語言、方程的特色,將文本中的一個個的字符,逐個提取成這個語言的合法詞語的過程,在這個示例中,就是把2+3-1提取出2,+,3,-,1五個詞的過程。若是是20+30-11,就得能提取出20,+, 30,-,11;若是是20+-1就得提取出20,+,-,1。

咱們看到了最後一個式子的錯誤,這個不歸詞法分析管,後面語法分析負責找出這類問題。

詞法分析具體的實現通常是經過正則表達式,正則規定出語言全部的操做、變量、各類類型的值的正則,詞法分析器逐個去匹配提取出詞語。

詞法分析正則表達式:

INTEGER: 0|[1-9][0-9]*

PLUS: +

MINUS: -

語法分析

在詞法分析基礎上,語法分析就能夠進行了,語法分析比詞法分析複雜一些,首先須要指定咱們的語言的語法規則:

  1. 一個塊級語句是表達式,數值和操做符
  2. 表達式數量任意
  3. 表達式是指數值+操做符+數值的組合
  4. 操做符包括「+」和「-」

依據這些規則設計出語法分析器,語法分析器判斷詞法分析器輸入的詞拼起來是否知足語法規則,知足後構建出parse tree。

語法分析規則:

expression := term operation term

operation := PLUS | MINUS

term := INTEGER | expression

context free grammer

通常的語法分析均可以經過BNF的格式來實現,上述的語法規則示例就是一種BNF。

可以只經過單純的BNF就徹底描述清楚並實現的語法被稱做"context free grammer",也就是不依賴上下文的語法,只要當前這段語句分析了就能有肯定的意思,一個詞彙不會有兩個意思。

我理解這個「肯定的意思」並不包含運行時層面的一些東西,主要是指語法上一個詞彙會不會有歧義,沒有歧義的就是context free grammer。有歧義的就不是,parse的過程就會更復雜,例如HTML就不是,後續會講到,因此在這裏提這個概念。

HTML Parser

not context free

具體到 HTML Parser,首先就是Html的語法不是context free的,爲何呢?

首先XML是Context free的,每個標籤都須要閉合,標籤之間也有明確的包含關係,使每個標籤都有肯定的含義,因此Html不是context free的緣由不在於它的基礎語法,而是由於它的包容性。

好比容許br標籤不閉合,甚至容許用</br>和<br>兩種寫法,好比標籤之間沒有造成嵌套關係<div><p></div></p>也不會報錯,會推斷修復這類問題。

DOM

Dom元素咱們都熟悉,在瀏覽器調試窗口element一欄就能夠看到咱們的html+JavaScript生成的Dom結構(這裏只說Html)。

所謂HTML的parsing過程就是把HTML的語法寫出的文本轉化成Dom tree的過程,所以用html標記語言以及JavaScript操做Dom元素的過程也被稱做Dom編程。

例以下面的代碼會被parse成下面的Dom tree。

<html>

<body>

<p>

Hello World

</p>

<div> <img src="example.png"/></div>

</body>

</html>

 

 

Dom的官方規範見這個連接:    https://www.w3.org/DOM/DOMTR

parse流程

 

 

執行parse的流程也是詞法分析和語法分析,tokeniser即詞法分析,tree construction爲語法分析部分

tokeniser

 

 

如上圖所示,主要過程就是定位出每個尖括號包裹的標籤,包括打開標籤和關閉標籤分別捕獲

tree construction

 

 

經過找到的標籤,給每一個打開標籤添加到樹中,直到閉合標籤以前的其餘標籤成爲本身的children,若是有特殊狀況特殊處理。

CSS parsing

同HTML不一樣,CSS是context free grammer,在此簡單列出一組CSS的詞法分析和語法分析的規則,只作簡要介紹

詞法分析:

comment \/\*[^*]*\*+([^/*][^*]*\*+)*\/

num [0-9]+|[0-9]*"."[0-9]+

nonascii [\200-\377]

nmstart [_a-z]|{nonascii}|{escape}

nmchar [_a-z0-9-]|{nonascii}|{escape}

name {nmchar}+

ident {nmstart}{nmchar}*

詞法分析也是基於正則表達式,定位CSS文本文檔中一個個的關鍵詞,其中name是id,ident是classname,從中能夠看出css的classname容許輸入哪些字符,不然會報錯。

語法分析:

 

 

這個比較抽象,看一組例子,一個CSS樣式文本和命中的規則:

 

 

Webkit CSS Parser

 

 

如上圖所示,一個webkit parse出的style rules也是一個樹形結構,第二層是一個一個的CSS rule,每一個rule都有分支,用來存放全部的selector和存放屬性的聲明。

render tree(frame tree)

從dom tree到render tree

通過了parse,咱們知道已經獲得了dom tree和style rules,接下來的過程就是從將二者合併成一個render tree,而且計算出真正render的必要信息。

要理解這句話,就要說清楚到dom tree這一步進展到了什麼程度,到render tree這一步又進展到了什麼程度。由於從整體上來講,構建出Dom->rendertree->layout->draw是很抽象的說法,具體到好比width有個width:30%,在哪一步計算出了絕對寬度值,到哪一步真正給dom對象設置了這個寬度,到哪一步真正把這個對象按照這個大小布置出來了,佈置出來以後何時繪製出來的。

下面首先簡單說一下我對dom tree和render tree分界線的理解,也就是dom tree和style rules這兩個東西都包括什麼,進展到了哪一步:

dom tree實現了一個樹形的dom object,一層一層的都從HTML文檔轉化爲HTMLObject,而且樹形都轉化成了HTMLObject的屬性。style rules(css rules)把CSS文檔轉換成了一組規則對象,每一個對象都包含了對應的css selector和css屬性和值,從文檔變成了對象。而後就沒有了,這個對象尚未真正開始計算樣式的實際值,沒有真正開始計算最終繪製顏色像素相關的東西。

這個計算實際值、計算最終繪製須要的一些內容的過程就是合併這二者構建render tree的過程。抽象來講,render tree包含了最終layout和paint(或者叫reflow和redraw)須要的必要信息,並且都各類樣式都計算出了最終的值,所謂layout和paint的過程也是調用了render tree上元素的layout和paint兩個方法,一個典型的render tree對象類以下所示:

 

 

render tree和dom tree的關係

render tree和dom tree不是一對一的關係,例如display:none的element不會體如今rendertree中;可是屬性hidden會繪製render tree object;select dom元素會繪製3個render object。

 

 

 

                                                            dom tree 到render tree

 

計算CSS合併Dom和css rules

要將CSS和Dom合併面臨着三個大問題:

  1. 樣式表可能十分龐大,可能另內存吃緊
  2. 爲每個dom元素遍歷樣式表會是很是浩繁的工程
  3. css樣式可能擁有很複雜的selector不便於對應

共享樣式信息

瀏覽器進行了一些設計來解決這些問題,首先是共享樣式信息。

若是兄弟元素知足了一系列的條件,那麼他們就共享樣式對象,不用重複計算。

Firefox rule tree

另外,爲了解決上述問題的1和3,firefox設計出了一種rule tree+style context tree的結構。

 

 

這一點和webkit有所不一樣,webkit含有相似的東西,但沒有生成這樣完整的tree。

rule tree+style context的實現有點複雜,主要目的是經過這樣的一顆樹結構化保存計算過得樣式信息,用於複用,提升性能。我也沒有特別搞懂,所以不詳細介紹理論,直接上個例子:

有以下html:

 

 

以下Css樣式表:

 

 

依據html生成的dom tree,取到一個dom節點後,便利樣式表招到匹配的樣式,根據匹配程度由低到高,由上到下列出這個dom全部匹配的樣式,生成rule tree:

 

 

 

例如針對第一個div元素,從上到下生成了B、C、D三個節點依次表明了1/2/5三條規則,優先級從低到高;這裏順便介紹一下爲何要生成這個tree,爲何知道優先級從低到高了,還要保留低優先級的:

  1. 這只是一個規則的從低到高的排列,並非具體的樣式屬性的,有些高優先級裏沒有配置的屬性,可能匹配的低優先級規則裏有配置
  2. 所有保留便於複用,假設此時開始找第二個div元素的匹配屬性,找到了1,2,6,此時1,2已經在rule tree上,只須要再添加一個6就能夠了,上面兩個作到了複用。

給一個元素在rule tree上生成了一個從上到下(優先級從低到高)的path以後,就能夠給元素生成對應的style context了。具體生成的style context以下圖所示:

 

 

仍是拿第一個div舉例子,在生成其 style context的時候,就把匹配第一個div元素的path最下面的樣式做爲該元素的指定樣式,這就是style context。

而後根據style context和rule tree構建style structs。style structs基於樣式屬性的維度,也就是每個屬性構建一個struct來組成structs。在本例中針對第一個div構建color屬性,規則D中有color,搜索結束使用這個color的值;針對它的margin屬性則不一樣,規則D中沒有關於margin的配置,向上到C中也沒有,知道B中找到了執行B中的margin值。

還有額外的狀況就是找到path的頂層了也沒有,也就是沒有顯式聲明的樣式針對這個屬性,那麼久根據該屬性的具體狀況,若是是繼承式的屬性,就再去看父元素的path,若是不是繼承性的屬性,就直接取該屬性的默認值。

樣式表計算的優先級

來源的優先級

從低到高排列以下

  1. Browser declarations
  2. User normal declarations
  3. Author normal declarations
  4. Author important declarations
  5. User important declarations

其中Author是指網頁的做者,User是指在頁面上修改屬性的人,這個對咱們平常調試有幫助,說明已經配置的屬性,若是不加important沒法覆蓋網頁加載(做者)樣式。

Specify

優先級從高到低:

  1. style屬性裏的樣式
  2. ID中的樣式
  3. class等屬性中的樣式
  4. 元素標籤的樣式。

以上先計算高優先級的屬性出現的次數,若是同樣,再計算低優先級的屬性出現的次數

layout(reflow)&paint(draw)

從render tree到layout&paint

在render tree構建完成以後,一個新的tree造成了,其中的每一個元素都包含了全部最終的樣式屬性的引用,值都計算出了最終值,不是相對值;可是並無真正的拿這些屬性去繪製圖形和放置圖形的位置,只是具備了全部繪製圖形所需的完善的信息,不須要再對這些數值信息進行加工了。

此時調用layout方法便可以計算出每一個元素的佈局位置和尺寸等信息,包括z-index的信息,調用paint方法就能夠計算出元素的最終像素繪製信息。

layout

正如上文所說,layout以前並無真正計算出元素的座標和尺寸、z-index等信息,layout將經過display、width、height、postion、float、left、right、min-height、max-height等尺寸相關的屬性,計算出元素所在的x軸Y軸Z軸的信息和最終的尺寸信息。

dirty bit system

計算完尺寸信息以後,dom結構會不斷變化,呈現引擎有一個標記髒值的方法,經過標記新增或者改變屬性的元素爲dirty的方法,在下一次layout的時候沒必要所有layout,而是隻layout標記爲dirty的元素、元素的子元素和元素的迭代向上父元素(具體狀況視改變的屬性不一樣而不一樣)。具體來講,若是改變了全局的字號或者直接改變了viewport的大小,會觸發全局的layout,不然改變了元素的尺寸、字號等,則會觸發局部的layout;還有一些屬性改變不會觸發layout,會在paint中說明。

Layout過程

  1. 父renderer計算出本身的寬度。
  2. 放置子元素的位置,調用子renderer的layout(包括全局layout、子元素dirty等狀況),計算出子元素的寬度、高度。
  3. 父元素利用子元素累計的高度算出本身的高度(也可能須要重算本身的寬度),調用父元素的父元素給父元素本身設置高度。
  4. 將dirty位置false。

Painting

在layout以後,能夠對rendertree的成員進行painting。

Painting和layout同樣,有局部和全局兩種狀況。全局不用多說,說一下局部Painting的狀況。

根據render tree的變化,將繪製的須要從新繪製的renderer置爲disable,操做系統會認爲這個繪製區域已通過期(dirty),操做系統會把多個這樣的區域結合起來,一塊兒觸發一次paint事件,而後調用paint 線程執行重繪。

重繪順序

重繪針對painting階段的屬性(非layout相關屬性)會按照固定的順序操做,所以會按照逆序將對應的屬性壓入Stacking context棧,從後向前彈出執行,堆棧內容從後向前以下:

  1. background color
  2. background image
  3. border
  4. children
  5. outline

最小改變

在dom元素或屬性變動後,,瀏覽器會優化嘗試重繪(paint)或重排(layout)最少的內容。若是改變了一個元素的顏色或背景色,將只會repaint這個元素;若是改變了元素的position將不得不致使重排+重繪該元素及其子元素,有時候還須要重排其兄弟元素;添加dom元素也會致使本身及其父元素的重繪和重排。若是改變Html字體將會清空render相關緩存並對整個頁面重排和重繪。

呈現引擎(rendering engine)線程

呈現引擎通常是一個單獨的線程,他們一般都是該頁面的主線程,而網絡交互部分則會根據請求樹簡歷多個網絡線程(2-6個)。主線程是一個無限循環,監聽須要重繪和重排的事件做出對應的render。

相關文章
相關標籤/搜索