轉載請註明出處:http://blog.csdn.net/cnnzp/article/details/6590087
Webkit CSS引擎分析
瀏覽器CSS模組負責CSS指令碼解析,並為每個element計算出樣式。CSS模組雖小,計算量大,設計不好往往成為瀏覽器效能的瓶頸。CSS模組在實現上有幾個特點:CSS對象眾多(顆粒小而多),計算頻繁(為每個element計算樣式)。這些特性決定了webkit在實現CSS引擎上採取的設計,演算法。如何高效的計算樣式是瀏覽器核心的重點也是痛點。
前端工程師可能更關注:
- 能被瀏覽器高效執行的CSS指令碼
瀏覽器核心工程師可能更關注:
- CSS內部資料的組織
- 計算樣式
- 思考
- 總結
高效執行的CSS指令碼
我這裡僅從webkit執行的效能上來講高效的CSS,不涉及CSS設計問題。
- 如果兩個或多個element的computedStyle不通過計算可以確認他們相等,那麼這些computedStyle相等的elements只會計算一次樣式,其餘的僅僅共用該computedStyle。
例如:
<table><tr class='row'><td class='cell' width=300 nowrap>Cell One</td></tr><tr class='row'><td class='cell' width=300 nowrap>Cell Two</td></tr>
那麼兩個tr共用computedStyle, 兩個td共用computedStyle。在核心裡,只會計算第一個tr和第一個td的ComputedStyle。
那麼如何做到共用computedStyle呢:
- 該共用的element不能有id屬性且CSS中還有該id的StyleRule.哪怕該StyleRule與Element不匹配。譬如:
div#id1{color:red}<p>paragraph1</p><p id="id1">paragraph2</p>
可以看到這兩個p標籤computedStyle本來是一樣的,但他們不共用。
- tagName和class屬性必須一樣。
- mappedAttribute必須相等。
- 不能有style屬性。哪怕style屬性相等,他們也不共用。例如:
<p style="color:red">paragraph1</p><p style="color:red">paragraph2</p>
他們並不共用computedStyle。
- 不能使用sibling selector,譬如:first-child, :last-selector, + selector.
- 使用id selector非常的高效。在使用id selector的時候需要注意一點:因為id是唯一的,所以不需要既指定id又指定tagName。例如:
Badp#id1 {color:red;}Good#id1 {color:red;}
- 使用class selector的策略與id selector一樣。在核心實現上,id selector與class selector的匹配並沒有多大的區別。如果同一個class需要賦予不同的css,你可以這樣做
Badp.class1 {color:red;}div.class1 {color:black;}Goodp-class1{color:red;}div-class1{color:black;}
當然這樣會造成網頁中的className增多。具體您決定怎麼取捨。
有時要選擇的node比較深時,我們可以採取如下寫法:
Baddiv > div > div > p {color:red;}Goodp-class{color:red;}
ChildSelector的匹配比較的慢。
- 不到萬不得已,不要使用attribute selector。例如:p[att1="val1"]。這樣的匹配非常慢。更不要這樣寫:p[id="id1"]。這樣將id selector退化成attribute selector。
Badp[id="id1"]{color:red;}p[class="class1"]{color:red;}Good#id1{color:red;}.class1{color:red;}
- 依賴繼承。如果某些屬性可以繼承,那麼自然沒有必要在寫一遍。
- 其他的selector在核心實現上很難做出最佳化,所以如果可以的話盡量不用。
Webkit CSS模組實現
這裡我更多的希望分享我實際開發CSS中所碰到的一些問題,透過這些問題來看webkit的設計也許更有體會。
一些名詞的解釋
有些webkit核心使用的名詞這裡作下解釋,如果對這些名詞不理解,那麼對研究代碼有一定的阻力
- mappedAttribute: 一些可以影響CSS ComputedStyle的html屬性。
舉個例子:<p align="middle">paragraph</p>
那麼屬性align="middle"就叫做mappedAttribute。一般大家都知道每個Element有個inlineStyleDeclaration,實際上還有個隱含的Declaration叫MappedStyleDeclaration.他的優先順序比普通的CSS高,比inlineStyle要低。
- renderStyle:這就是大家熟悉的ComputedStyle在webkit中的表示。
- bloom filter:一種演算法。沒接觸過的可以網上搜尋。
CSS內部資料的組織
這裡不想畫一些css對象的繼承圖。對象繼承圖可以參考這篇文章。並且我假設讀者已經熟知CSS相關規範,概念。
解析完CSS指令碼後,會產生CSSStyleSheetList,他儲存在Document對象上。為了更快的計算樣式,必須對這些CSSStyleSheetList進行重新組織。(思考,你能直接從CSSStyleSheetList上計算樣式嗎?)
計算樣式就是從CSSStyleSheetList中找出所有匹配相應元素的property-value對。匹配會通過CSSSelector來驗證,同時需要滿足層疊規則。
一種簡單但效率偏低的組織方式,暫且稱之為數組模型。
將所有的declaration中的property組織成一個大的數組。數組中的每一項紀錄了這個property的selector,property的值,權重(層疊規則)。例如:
<style> p > a{color:red; background-color:black;} a {color:yellow} div{margin:1px;} </style>重新組織之後的數組資料為(weight我只是表示了他們之間的相對大小,並非實際值。): selector property weight1, a color:yellow 12, p > a color:red 23, p > a background-color:black 24, div margin:1px 3
可以看到每一個property成為數組的一項。相同的tagName在數組中的位置相鄰,譬如selector a 和selector p > a在數組中相鄰。所有的property以selector的tagName順序存放。有了這樣的數組組織,你可以想想了,該如何計算樣式呢?
一種高效的組織方式,暫且稱之為hash模型。
webkit使用CSSRuleSet對象來組織這些資料。CSSRuleSet是這樣一個對象:他內部有4個hash表,分別為idRules, classRules, tagNameRules, universalRules。
這些hash表的定義是這樣的:HashMap<String*, CSSRuleDataList*>
CSSRuleDataList是一個list,其總每一項為CSSRuleData。
CSSRuleData儲存了一個css的styleRule,以及這個styleRule的selector的specificity(可以理解成權值)。在CSSRuleData的constructor中會計算selector的bloom filter值。
為一個粗略的圖示,我並沒有完整的畫出各個類的定義,但已經可以協助我們理解這些類的關係:
計算樣式
樣式的計算如果設計的不當,直接影響瀏覽器核心的效能,所以這裡的演算法值得大家仔細的分析。
數組模型我們將匹配之後的結果放在一個數組中,這個數組初始size為CSS property的個數。暫且稱這個數組為結果數組。
- 將default stylesheet, user stylesheet, author stylesheet組織成一個大的數組之後。要匹配一個標籤,譬如:
<p><a href="#">link</a></p>計算標籤a的樣式:
因為數組的tagName是順序的,所以可以使用二分尋找法,找到a的開始位置和結束位置,此時為1-->3.
- 針對數組的每一項進行check selector,如果check selector成功,存放在結果數組中。
- 將匹配的結果存放在結果數組中的時候,需要判斷結果數組中是否已經有了該property,如果已經存在則需要比較這兩個property的權值,如果新的property權重大於老的,那麼需要替換數組中的這一項。
- 對數組中所有tagName為universaltagName進行匹配。重複2-3。
總結:可以看到該演算法需要匹配所有tagName相同的項,以及所有universaltagName。在checkselector成功之後插入結果數組中,還需要判斷是否已經存在了該property。
還有一個更嚴重的問題,該演算法在checkselector的時候,沒有儲存匹配的selector的相關資訊,為以後的局部更新帶來了非常多的不確定性問題,導致局部排版無法判斷是否需要重排。對比webkit存在非常多的動作來將不確定的因素確定化,來最佳化排版所需要的動作。
hash模型在數組模型中,計算的結果存放在一個數組中。在hash模型中,也是將計算的結果存放在一個數組中:
Vector<const RuleData*, 32> m_matchedRules;
- 首先判斷該element是否存在可以共用的renderStyle。是否可以共用的條件較多,這裡不詳述,粗略的可以參看這裡。但這個策略非常非常棒,網頁中能共用的標籤非常多,所以能極大的提升執行效率!如果能共用,那就不需要執行匹配演算法了,執行效率自然非常高。
我在對www.sina.com.cn的測試中, 17864次計算樣式,有4764次共用。將近27%的計算樣式的過程不需要進行,意味著此處效能提高約27%左右!該網站共有9686個node,3412個element。
- 依次匹配default StyleSheet , user StyleSheet, author StyleSheet,並將結果存放在結果數組中。並記錄各種stylesheet匹配結果在結果數組中的起始位置。
每一個StyleSheet匹配的演算法:
- 如果該Element有id屬性,那麼從CSSRuleSet的id hash table中取出相應的CSSRuleDataList
- 依次測試CSSRuleDataList中的每一項CSSRuleData。這裡首先會利用bloom filter演算法過濾掉不合格CSSStyleRule。
- 在check selector過程中,如果匹配成功將其加入到結果數組中。
- 根據權值對結果數組進行排序。
- 如果該Element有class屬性,那麼從CSSRuleSet的class hash table中取出相應的CSSRuleDataList。重複執行步驟2-->步驟4
- 根據Element的tagName,從CSSRuleSet種取出tagName對應的CSSRuleDataList,重複步驟2-->步驟4.
- 對所有universaltagName的CSSRuleDataList重複步驟2-->步驟4.
上述步驟產生結果數組的演算法流程圖如下:
- 得到了所有匹配的stylerule之後,需要根據這個結果產生renderstyle。演算法步驟如,請看圖的時候注意兩個問題:
- 如何體現層疊規則中不同樣式表的權重。
- 如何體現同一個樣式表中相同的property,相同的權重,後面的覆蓋前面的。
一些思考題
在這一篇文檔中實在很難將一個核心模組的所有問題闡述清楚,所以我這裡列舉一些問題供大家討論學習。這些問題也是我在實際工具做所碰到一些具體問題,沒有實際開發過CSS模組很難體會到這些問題,而且webkit對這些問題處理的很好,給了我很多啟發。
- 如果style標籤寫在了body地區,webkit在解析完這個style標籤如何做呢?style標籤寫在了body地區好嗎?
- :hover偽類在CSS應用很廣泛,想想瀏覽器核心該怎麼做?需要在一個element收到hover狀態的時候,重新計算css嗎?
- CSS對象粒度小但數目大,大到CSSStyleSheetList,小到一個CSSValue都要使用CSS對象來表示。而CSS文檔又比較大,這樣記憶體會不會片段太多?
- CSS對象粒度小但數目大,重複分配釋放除了片段大,也很花費時間,想想有什麼好辦法嗎?考慮自己管理CSS對象的記憶體?
- CSS對象粒度小但數目大,這些小粒度對象能共用嗎?設計模式裡有個Flyweight模式,CSS裡有很好體現。
- 一個Element的class屬性或者id屬性變了,需要重新計算renderstyle以看是否需要重排這個Element。我們知道有sibling selector
p.class1 + a
那是不是這個改變了屬性的Element的所有兄弟都需要重新計算renderstyle或者說重新排版呢?
- 有:first-child偽類,那是不是意味著往父親節點中插入一個頭結點,所有的孩子都需要重排呢?因為他們可能有:first-child selector。webkit怎麼做呢?
- css value中有個值為inherit,表示使用他的parent的屬性值。如果他的parent的屬性值變了呢,孩子的值如何更新?
- 這篇文章對bloom filter演算法在css中應用講的不多,但這也確實對CSS check selector進行了不少的最佳化。有興趣的讀者可以參考以下三點去看webkit源碼:
- 在產生CSSRuleData對象的時候,有bloom filter資料產生。
- 在parse html的時候,每個element的beginParsingChildren事件中會更新bloom filter。
- 在matchRulesForList的時候,方法fastRejectSelector就是過濾發生的地方。
注意:該最佳化的動作只在新的webkit版本才有,較老的webkit版本沒有此動作。
- computedStyle記錄了每個Element的所有的property的值,瀏覽器排版引擎會非常頻繁的從computedStyle中取出某個property的值,將computedStyle設計成一個數組可以嗎?webkit使用renderStyle這個對象來表示computedStyle,這個對象在設計上有什麼優點?重點應關注兩點:
- 這個對象比數組的形式節省了非常多的記憶體。
- 這個對象比數組的形式節省了非常多的檢索時間。
總結
CSS引擎做的事情非常少(解析和計算樣式),往往被大家忽略,但要設計出靈活高效的CSS引擎確實不易。通過剖析webkit CSS的實現,經常有些設計的亮點讓我激動很久,所以不要忽略webkit css模組,他會給你驚喜!