綜述css
以前使用ExtJS時遇到一個問題:爲何依次設置多個組件的可見性界面會卡頓?在瞭解HTML的dom操做相關內容的時候也好奇這個東西究竟是怎麼回事,而後尤爲搞不懂CSS和Html分管樣式和網頁結構,這個東西是怎麼實現的,是否是很複雜?html
帶着這些問題,看了一些文章,尤爲是據說了Redraw和Reflow的概念以後,開始研究了dom的性能調優,最近看了一篇《how browser work》,以爲寫得很詳細,結合以前看的文章,解決了很多的困惑,寫一篇對這個文章的讀後總結,順便記下來本身掌握的一些瀏覽器性能的知識。web
瀏覽器的整體結構正則表達式
瀏覽器主要部件包括:express
具體結構以下圖所示:編程
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: -
語法分析
在詞法分析基礎上,語法分析就能夠進行了,語法分析比詞法分析複雜一些,首先須要指定咱們的語言的語法規則:
依據這些規則設計出語法分析器,語法分析器判斷詞法分析器輸入的詞拼起來是否知足語法規則,知足後構建出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合併面臨着三個大問題:
共享樣式信息
瀏覽器進行了一些設計來解決這些問題,首先是共享樣式信息。
若是兄弟元素知足了一系列的條件,那麼他們就共享樣式對象,不用重複計算。
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,爲何知道優先級從低到高了,還要保留低優先級的:
給一個元素在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,若是不是繼承性的屬性,就直接取該屬性的默認值。
樣式表計算的優先級
來源的優先級
從低到高排列以下
其中Author是指網頁的做者,User是指在頁面上修改屬性的人,這個對咱們平常調試有幫助,說明已經配置的屬性,若是不加important沒法覆蓋網頁加載(做者)樣式。
Specify
優先級從高到低:
以上先計算高優先級的屬性出現的次數,若是同樣,再計算低優先級的屬性出現的次數
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過程
Painting
在layout以後,能夠對rendertree的成員進行painting。
Painting和layout同樣,有局部和全局兩種狀況。全局不用多說,說一下局部Painting的狀況。
根據render tree的變化,將繪製的須要從新繪製的renderer置爲disable,操做系統會認爲這個繪製區域已通過期(dirty),操做系統會把多個這樣的區域結合起來,一塊兒觸發一次paint事件,而後調用paint 線程執行重繪。
重繪順序
重繪針對painting階段的屬性(非layout相關屬性)會按照固定的順序操做,所以會按照逆序將對應的屬性壓入Stacking context棧,從後向前彈出執行,堆棧內容從後向前以下:
最小改變
在dom元素或屬性變動後,,瀏覽器會優化嘗試重繪(paint)或重排(layout)最少的內容。若是改變了一個元素的顏色或背景色,將只會repaint這個元素;若是改變了元素的position將不得不致使重排+重繪該元素及其子元素,有時候還須要重排其兄弟元素;添加dom元素也會致使本身及其父元素的重繪和重排。若是改變Html字體將會清空render相關緩存並對整個頁面重排和重繪。
呈現引擎(rendering engine)線程
呈現引擎通常是一個單獨的線程,他們一般都是該頁面的主線程,而網絡交互部分則會根據請求樹簡歷多個網絡線程(2-6個)。主線程是一個無限循環,監聽須要重繪和重排的事件做出對應的render。