Webkit CSS引擎分析

來源:互聯網
上載者:User
轉載請註明出處:http://blog.csdn.net/cnnzp/article/details/6590087
Webkit CSS引擎分析

瀏覽器CSS模組負責CSS指令碼解析,並為每個element計算出樣式。CSS模組雖小,計算量大,設計不好往往成為瀏覽器效能的瓶頸。CSS模組在實現上有幾個特點:CSS對象眾多(顆粒小而多),計算頻繁(為每個element計算樣式)。這些特性決定了webkit在實現CSS引擎上採取的設計,演算法。如何高效的計算樣式是瀏覽器核心的重點也是痛點。

    前端工程師可能更關注:
  1. 能被瀏覽器高效執行的CSS指令碼
    瀏覽器核心工程師可能更關注:
  1. CSS內部資料的組織
  2. 計算樣式
  3. 思考
  4. 總結
高效執行的CSS指令碼

我這裡僅從webkit執行的效能上來講高效的CSS,不涉及CSS設計問題。

  1. 如果兩個或多個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呢:

    1. 該共用的element不能有id屬性且CSS中還有該id的StyleRule.哪怕該StyleRule與Element不匹配。譬如:

      div#id1{color:red}<p>paragraph1</p><p id="id1">paragraph2</p>

      可以看到這兩個p標籤computedStyle本來是一樣的,但他們不共用。

    2. tagName和class屬性必須一樣。
    3. mappedAttribute必須相等。
    4. 不能有style屬性。哪怕style屬性相等,他們也不共用。例如:
      <p style="color:red">paragraph1</p><p style="color:red">paragraph2</p>

      他們並不共用computedStyle。

    5. 不能使用sibling selector,譬如:first-child, :last-selector, + selector.
  2. 使用id selector非常的高效。在使用id selector的時候需要注意一點:因為id是唯一的,所以不需要既指定id又指定tagName。例如:
    Badp#id1 {color:red;}Good#id1 {color:red;}
  3. 使用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的匹配比較的慢。

  4. 不到萬不得已,不要使用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;}
  5. 依賴繼承。如果某些屬性可以繼承,那麼自然沒有必要在寫一遍。
  6. 其他的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值。

為一個粗略的圖示,我並沒有完整的畫出各個類的定義,但已經可以協助我們理解這些類的關係:

  • 將default stylesheet, userstylesheet, authorstylesheet存放在不同的CSSRuleSet上。而數組模型會將所有的stylesheet組織到一個數組中。不要小瞧這步動作,這已經讓我眼前一亮,他關係到後面的匹配演算法部分。
  • 每個CSSRuleSet將所有的stylerule分別組織到idRules, classRules, tagNameRules, universalRules。譬如:
    #id1{color:red;}    -->存放在idRules中。.class1{color:red;} -->存放在classRules中。.class1{color:red;} -->同上。p{color:red;}       -->在tagNameRules中。*{color:red;}       -->在universalRules中。
  • 核心在在當前所有的stylesheet都已經請求結束,CSS parser結束之後進行組織資料這個動作。

計算樣式

樣式的計算如果設計的不當,直接影響瀏覽器核心的效能,所以這裡的演算法值得大家仔細的分析。

數組模型我們將匹配之後的結果放在一個數組中,這個數組初始size為CSS property的個數。暫且稱這個數組為結果數組。

  1. 將default stylesheet, user stylesheet, author stylesheet組織成一個大的數組之後。要匹配一個標籤,譬如:

    <p><a href="#">link</a></p>計算標籤a的樣式:

    因為數組的tagName是順序的,所以可以使用二分尋找法,找到a的開始位置和結束位置,此時為1-->3.

  2. 針對數組的每一項進行check selector,如果check selector成功,存放在結果數組中。
  3. 將匹配的結果存放在結果數組中的時候,需要判斷結果數組中是否已經有了該property,如果已經存在則需要比較這兩個property的權值,如果新的property權重大於老的,那麼需要替換數組中的這一項。
  4. 對數組中所有tagName為universaltagName進行匹配。重複2-3。

總結:可以看到該演算法需要匹配所有tagName相同的項,以及所有universaltagName。在checkselector成功之後插入結果數組中,還需要判斷是否已經存在了該property。

還有一個更嚴重的問題,該演算法在checkselector的時候,沒有儲存匹配的selector的相關資訊,為以後的局部更新帶來了非常多的不確定性問題,導致局部排版無法判斷是否需要重排。對比webkit存在非常多的動作來將不確定的因素確定化,來最佳化排版所需要的動作。

hash模型在數組模型中,計算的結果存放在一個數組中。在hash模型中,也是將計算的結果存放在一個數組中:
Vector<const RuleData*, 32> m_matchedRules;

  1. 首先判斷該element是否存在可以共用的renderStyle。是否可以共用的條件較多,這裡不詳述,粗略的可以參看這裡。但這個策略非常非常棒,網頁中能共用的標籤非常多,所以能極大的提升執行效率!如果能共用,那就不需要執行匹配演算法了,執行效率自然非常高。
    我在對www.sina.com.cn的測試中, 17864次計算樣式,有4764次共用。將近27%的計算樣式的過程不需要進行,意味著此處效能提高約27%左右!該網站共有9686個node,3412個element。
  2. 依次匹配default StyleSheet , user StyleSheet, author StyleSheet,並將結果存放在結果數組中。並記錄各種stylesheet匹配結果在結果數組中的起始位置。
      每一個StyleSheet匹配的演算法:
    1. 如果該Element有id屬性,那麼從CSSRuleSet的id hash table中取出相應的CSSRuleDataList
    2. 依次測試CSSRuleDataList中的每一項CSSRuleData。這裡首先會利用bloom filter演算法過濾掉不合格CSSStyleRule。
    3. 在check selector過程中,如果匹配成功將其加入到結果數組中。
    4. 根據權值對結果數組進行排序。
    5. 如果該Element有class屬性,那麼從CSSRuleSet的class hash table中取出相應的CSSRuleDataList。重複執行步驟2-->步驟4
    6. 根據Element的tagName,從CSSRuleSet種取出tagName對應的CSSRuleDataList,重複步驟2-->步驟4.
    7. 對所有universaltagName的CSSRuleDataList重複步驟2-->步驟4.
  3. 上述步驟產生結果數組的演算法流程圖如下:

  4. 得到了所有匹配的stylerule之後,需要根據這個結果產生renderstyle。演算法步驟如,請看圖的時候注意兩個問題:
    1. 如何體現層疊規則中不同樣式表的權重。
    2. 如何體現同一個樣式表中相同的property,相同的權重,後面的覆蓋前面的。

一些思考題

在這一篇文檔中實在很難將一個核心模組的所有問題闡述清楚,所以我這裡列舉一些問題供大家討論學習。這些問題也是我在實際工具做所碰到一些具體問題,沒有實際開發過CSS模組很難體會到這些問題,而且webkit對這些問題處理的很好,給了我很多啟發。

  1. 如果style標籤寫在了body地區,webkit在解析完這個style標籤如何做呢?style標籤寫在了body地區好嗎?
  2. :hover偽類在CSS應用很廣泛,想想瀏覽器核心該怎麼做?需要在一個element收到hover狀態的時候,重新計算css嗎?
  3. CSS對象粒度小但數目大,大到CSSStyleSheetList,小到一個CSSValue都要使用CSS對象來表示。而CSS文檔又比較大,這樣記憶體會不會片段太多?
  4. CSS對象粒度小但數目大,重複分配釋放除了片段大,也很花費時間,想想有什麼好辦法嗎?考慮自己管理CSS對象的記憶體?
  5. CSS對象粒度小但數目大,這些小粒度對象能共用嗎?設計模式裡有個Flyweight模式,CSS裡有很好體現。
  6. 一個Element的class屬性或者id屬性變了,需要重新計算renderstyle以看是否需要重排這個Element。我們知道有sibling selectorp.class1 + a那是不是這個改變了屬性的Element的所有兄弟都需要重新計算renderstyle或者說重新排版呢?
  7. 有:first-child偽類,那是不是意味著往父親節點中插入一個頭結點,所有的孩子都需要重排呢?因為他們可能有:first-child selector。webkit怎麼做呢?
  8. css value中有個值為inherit,表示使用他的parent的屬性值。如果他的parent的屬性值變了呢,孩子的值如何更新?
  9. 這篇文章對bloom filter演算法在css中應用講的不多,但這也確實對CSS check selector進行了不少的最佳化。有興趣的讀者可以參考以下三點去看webkit源碼:
    1. 在產生CSSRuleData對象的時候,有bloom filter資料產生。
    2. 在parse html的時候,每個element的beginParsingChildren事件中會更新bloom filter。
    3. 在matchRulesForList的時候,方法fastRejectSelector就是過濾發生的地方。

    注意:該最佳化的動作只在新的webkit版本才有,較老的webkit版本沒有此動作。

  10. computedStyle記錄了每個Element的所有的property的值,瀏覽器排版引擎會非常頻繁的從computedStyle中取出某個property的值,將computedStyle設計成一個數組可以嗎?webkit使用renderStyle這個對象來表示computedStyle,這個對象在設計上有什麼優點?重點應關注兩點:
    1. 這個對象比數組的形式節省了非常多的記憶體。
    2. 這個對象比數組的形式節省了非常多的檢索時間。
總結

CSS引擎做的事情非常少(解析和計算樣式),往往被大家忽略,但要設計出靈活高效的CSS引擎確實不易。通過剖析webkit CSS的實現,經常有些設計的亮點讓我激動很久,所以不要忽略webkit css模組,他會給你驚喜!

相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.