更快的火狐!超快速 CSS 引擎:Quantum CSS

【譯】

你可能聽說過 Quantum 項目…… 它是 Firefox 內部組件的重大改寫,為了讓 Firefox 更快。 開發者對測試版瀏覽器 Servo 做組件替換,同時對引擎的其它部分進行改進。

這一項目就像給正在飛行的噴氣式飛機更換髮動機。 開發者逐個組件做適當修改,一旦某個組件完成,馬上可以看到效果。

Servo 的第一個主要組件——Quantum CSS 引擎(曾經叫做 Stylo)——已經在每日構建版本中測試。 要確保該功能打開,可以進入 about:config,設置 layout.css.servo.enabled 為 true。

這一引擎吸收了四種瀏覽器的最先進創新技術,創建了新的超級 CSS 引擎。

它充分利用現代化硬體的優勢,在機器的全部核心上並行化執行工作任務。 這意味著它能以 2 倍 或者 4倍 甚至 18 倍的速度運行。

更重要的是,它結合了來自其它瀏覽器的優化方案。 即使沒有並行化執行,也會是一個快速的 CSS 引擎。

那麼,什麼是 CSS 引擎呢? 首先來了解 CSS 引擎, 它如何與瀏覽器的其餘部分配合。 然後再看看 Quantum CSS 如何實現速度更快。

CSS 引擎做什麼?

CSS 引擎是瀏覽器渲染引擎的一部分。 渲染引擎獲取網站的 HTML 和 CSS 文件,並將其轉換為屏幕上的像素。

每個瀏覽器都有渲染引擎。 Chrome 中叫作 Blink。 Edge 中叫作 EdgeHTML。 Safari 中叫作 WebKit。 而 Firefox 中叫作 Gecko。

從文件轉換為像素,所有渲染引擎在做基本類似的事情:

  1. 將文件解析為瀏覽器可以理解的對象,包括 DOM。 在這一步,DOM 獲知頁面的結構。 它知道元素之間的父/子關係。 卻並不知道這些元素如何顯示。

  1. 計算出這些元素的樣子。 對每一個 DOM 節點,CSS 引擎先確定適用哪個 CSS 規則。 然後計算該節點的每個 CSS 屬性的值。

  1. 確定每個節點的尺寸及其在屏幕上的位置。 為每個即將顯示在屏幕上的目標創建盒子。 盒子不只可以代表 DOM 節點…… 也可以是 DOM 節點內部的文本行。

  1. 繪製出不同的盒子。 可能是在多個層上進行繪製。 這就像過去的手繪動畫,用的是半透明薄紙層。 如果需要,可以只修改一個層,而不必重繪其它層的東西。

  1. 對這些圖層,應用合併操作(如變形),並將它們轉化為圖像。 就像給疊起來的圖層拍攝照片一樣。 圖像將在屏幕上呈現。

也就是說,當開始計算樣式時,CSS 引擎做兩件事:

  • DOM 樹
  • 樣式規則列表

它逐個遍歷每個 DOM 節點,計算該節點的樣式。 並給出 DOM 節點每個/全部屬性的值,即使樣式表沒有聲明該屬性的值。

就像在填寫表格。 為每個 DOM 節點填寫一份這樣的表格。 表格的每個欄位都需要填寫一個答案。

要實現這一點,CSS 引擎需要做兩件事:

  • 確定哪些規則適用於節點 —— 通過 選擇器匹配
  • 使用父節點或默認值填充缺失的值 —— 通過 層疊

選擇器匹配

這一步驟中,把和 DOM 節點匹配的所有規則添加到一個列表。 因為可能匹配到多個規則,所以同一屬性就可能出現多個聲明。

另外,瀏覽器自身也添加了默認 CSS 規則(用戶代理樣式表)。 CSS 引擎如何知道應該選擇哪個值呢?

這裡就有優先順序規則出現了。 CSS 引擎創建一個表格。 然後根據不同列對聲明做排序。

具有最高優先順序的規則勝出。 根據這個表格,CSS 引擎填充已經可以確定的值。

其餘的值,則使用層疊。

層疊(cascade)

層疊使 CSS 易於編寫和維護。 使用層疊,可以在 body 上設置 color 屬性,p 和 span 和 li 中的文本都會使用該顏色(除非有一個更高優先順序的覆蓋)。

CSS 引擎在表格里查看空白盒子。 如果該屬性默認繼承,那麼 CSS 引擎就沿著樹形結構向上,查看祖先節點是否有值。 如果全部祖先節點都沒有值,或者該屬性不繼承,就獲得一個默認值。

這時已經為這個 DOM 節點計算了所有樣式。

旁註:樣式結構體共享

上面看到的表格可能有一點兒誤導性。 CSS 有成百上千的屬性。 如果 CSS 引擎保持每個 DOM 節點的每個屬性值,會很快耗盡內存。

因此,引擎會做 樣式結構體共享。 經常在一起的數據(如字體屬性)被保存在一個叫做樣式結構體的對象里。 不再將全部屬性保存在同一對象里,而是讓對象只持有指針,指向計算出的樣式。 對於每個類別,都有一個指針指向樣式結構體,具有此 DOM 節點應有的正確值。

這樣節省了內存和時間。 具有相似屬性的節點(如兄弟節點)可以把共享的屬性指向相同的結構體。 因為很多屬性是繼承的,如果該後代沒有特別指定覆蓋的話,祖先節點也可以與後代共享結構體。

現在,如何做到更快?

這是沒有優化時,樣式計算的樣子。

這裡發生了很多工作。 不僅第一次頁面載入時發生。 當用戶與頁面交互、滑鼠在元素上懸停、或更改 DOM 時,會反覆發生,觸發樣式重建。

這表明 CSS 樣式計算是很好的優化之選…… 過去 20 年裡瀏覽器們一直在測試不同的優化策略。 Quantum CSS 所做的是從不同瀏覽器引擎中擇取最好的策略,並把它們結合起來創造出一個超級快的新引擎。

下面來看看它們如何協同工作的細節。

全都並行運行

Servo 項目(Quantum CSS 來自於此)是一個實驗性的瀏覽器,它試圖把網頁渲染的所有部分並行化。 這是什麼意思呢?

計算機就像大腦。 一部分負責思考(算術邏輯單元/arithmetic logic unit, ALU)。 緊鄰著有一些短期記憶體(寄存器)。 它們在 CPU 上組合在一起。 還有更長期的記憶體,RAM(隨機存取存儲器/Random Access Memory, RAM)。

早期的計算機使用這種 CPU 僅能同時思考一件事。 但是過去的十多年裡,CPU 變為有多個 ALU 和寄存器,在 CPU 核心上組合在一起。 這樣 CPU 可以同時思考幾件事——並行。

Quantum CSS 利用這些最新計算機特性,將不同 DOM 節點的樣式計算分配給不同核心。

看起來很簡單…… 只要把樹按分支拆分,在不同核心上運行。 實際上更難一些,原因很多。 其中一個原因是 DOM 樹經常是不均勻的。 其中一個核需要比其它核做更多的工作。

為了平衡工作,Quantum CSS 使用一種叫做工作竊取(work stealing)的技術。 一個 DOM 節點在處理時,代碼將它的直屬子節點拆分為 1 個或多個「工作單元」。 這些工作單元被放進隊列。

當一個核心完成了隊列里的工作,它可以在其它隊列里獲得更多的工作。 意味著我們可以均勻的分配工作,而不需要提前遍歷整個樹去計算如何平衡。

大多數瀏覽器里,很難正確實現並行。 並行是已知的難題,CSS 引擎非常複雜。 並且處於渲染引擎中最複雜的兩個部分之間——DOM 和布局。 所以很容易產生錯誤,並行化可能會導致很難追蹤的錯誤,稱為數據競爭。 這類錯誤在另一篇文章中有詳細解釋。

如果成百上千的工程師都在貢獻代碼,並行化編程如何能不擔心? 這是我們引入 Rust 的目標。

使用 Rust,可以靜態驗證確認沒有數據競爭。 只要一開始不讓它們進入代碼,就可以避免棘手的調試錯誤。 編譯器不允許你這麼做。 這方面未來會寫更多文章介紹。 現在,你可以看 intro video about parallelism in Rust 或 more in-depth talk about work stealing。

這樣,CSS 樣式計算變成了並行問題——沒有什麼能阻止你高效的並行運行。 也意味著可以獲得線性的加速效果。 如果機器有 4 個核心,可以接近以 4 倍速度運行。

用規則樹(Rule Tree)加速樣式重建(restyle)

每個 DOM 節點,CSS 引擎需要遍歷全部規則來進行選擇器匹配。 對於大多數節點,匹配不經常變化。 例如,如果用戶將滑鼠懸停在父節點,它匹配的規則可能會變化。 仍然需要重計算它的後代節點的樣式來解決屬性繼承,但是後代節點匹配的規則可能並沒有變化。

所以最好記下哪個規則匹配了這些後代節點,這樣就不需要再次對它們做選擇器匹配了…… 這就是規則樹(借鑒了 Firefox 前一代 CSS 引擎)做的事。

CSS 引擎經過一個過程,找到那些可匹配的選擇器,然後對其按照優先順序排序。 這時,它就建好了規則鏈表。

該列表會添加到規則樹中。

CSS 引擎嘗試將樹中的分支數量保持最小。 為此,它將儘可能的復用分支。

如果一個列表中的大多數選擇器與已有分支相同,則沿用同樣的路徑。 但是可能有一個點,列表中的下一個規則不在這個分支上。 只在這一點才添加新分支。

DOM 節點得到一個指針,指向最後添加的規則(本例中,dev#warning 那條規則)。 這是最優先的一條。

樣式重建時,引擎先迅速檢查父節點的改變是否可能改變子節點匹配的規則。 如果不改變,對於任何後代,引擎可以直接根據後代節點的指針找到那條規則。 從那條規則,它能沿著規則樹向上找回根,得到匹配規則的完整列表,從最高優先順序到最低優先順序。 也就是說可以完全跳過選擇器匹配和排序的過程。

這有助於減少樣式重建過程的工作量。 但是初始化樣式時仍然有很多工作。 如果有 10,000 節點,仍然需要做 10,000 次選擇器匹配。 有另一種方法來加速。

使用樣式共享緩存(Style Sharing Cache)加速初始渲染(和層疊)

考慮有成千上萬個節點的頁面。 很多節點匹配相同規則。 例如,一個很長的 Wikipedia 頁面…… 主內容區域內的段落最終應該匹配完全相同的規則,具有完全相同的計算樣式。

如果不做優化,CSS 引擎必須對每個段落匹配選擇器並計算樣式。 但是如果有辦法證明段落與段落的樣式都相同,引擎就可以只做一次工作,把每個段落節點指向同一計算樣式。

這就是樣式共享緩存(受 Safari 和 Chrome 啟發)的做法。 一旦處理完一個節點,就將計算出的樣式放入緩存。 然後,在計算下一個節點的樣式之前,它會運行幾個校驗來看是否能使用緩存。

這些校驗包括:

  • 兩個節點是否有相同 id、class 等? 如果有,有可能匹配同一規則。
  • 任何不是基於選擇器的——如內聯樣式——節點是否有相同的值? 如果是,以上的規則或者不被覆蓋,或者以相同的方式覆蓋。
  • 兩者的父節點指向計算出的同一樣式對象? 如果是,繼承的值也將相同。

從一開始這些校驗就存在於早期的樣式共享緩存的實現中。 但是也有很多其它小案例,樣式可能匹配不上。 例如,如果 CSS 規則使用 :first-child 選擇器,兩個段落可能不匹配,即使以上校驗表明它們匹配。

在 WebKit 和 Blink 里,樣式共享緩存在這種情況下停止,不再使用緩存。 隨著越來越多網站使用這些現代的選擇器,這一優化變得用途越來越小,所以 Blink 團隊最近把它移除了。 但事實上還有一種方法,讓樣式共享緩存能跟上這些變化。

Quantum CSS 先匯總這些特別的選擇器,檢查它們是否適用於 DOM 節點。 然後將答案以 1 和 0 的形式存儲。 如果兩個元素具有相同的 1 和 0,就知道它們絕對匹配。

如果 DOM 節點能共享已經計算過的樣式,就可以跳過幾乎所有的工作。 因為頁面經常有很多 DOM 具有相同樣式,樣式共享緩存可以節省內存,並真的能加快速度。

結論

這是從 Servo 到 Firefox 的第一個重大技術轉移。 關於如何把 Rust 寫出的現代化、高性能代碼引進 Firefox 主幹,一路上我們學到很多。

我們很興奮,這一個大型的 Quantum 項目已經準備好讓用戶直接進行體驗。 我們很樂意讓你們試用,如果發現任何問題請告知。

公眾號推薦

知識分享行動

每天 10:24

只聊技術細節

掃碼立即參與

aHR0cDovL3dlaXhpbi5xcS5jb20vci9lblhZd0NqRXRuWEpyV3lqOXlCbw== (二維碼自動識別)


推薦閱讀:

張鑫旭:說說CSS學習中的瓶頸
從Chrome源碼看瀏覽器如何計算CSS

TAG:火狐浏览器Firefox | CSS | performance |