幾張圖程序員帶你揭祕一個超快的 CSS 引擎

編程語言 CSS 程序員 Firefox 孝道的重要性 孝道的重要性 2017-08-30

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

(圖片來自網絡)

或許你聽說過 Quantum 項目。 它是對於 Firefox 內部的一個重大改寫,以達到讓 Firefox 更快運行的目的。我們將實驗性的瀏覽器 Servo 的一部分功能調換出來,並對引擎的其他部分做除了重大的改進。

這個項目好比一架正在飛行的飛機的引擎。我們對適當的地方進行改進,一個一個組件地改進, 當著這些組件準備好的時候,你就能夠看到它對 Firefox 的影響。

這裡還是要推薦下我自己建的前端學習群:657137906,如果你正在學習前端,小編歡迎你加入,大家都是前端黨,不定期分享乾貨(只有web前端相關的),包括我自己整理的一份2017最新的前端資料和零基礎入門教程,歡迎初學和進階中的小夥伴。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

第一個來自 Servo 的主要組件就是一個全新的CSS 引擎,名為 Quantum CSS (之前稱作 Stylo) — 現在在瀏覽器 Nightly 版本中已經可以用於測試了。你可以進入about:config 並設置 layout.css.servo.enabled 以確保這個功能可以被使用。

這個新引擎將四個瀏覽器中最先進的革新技術結合在一起,創造出了這個超級 CSS 引擎。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

它充分利用了現代的計算機硬件,使你的計算機的所有核心並行工作。這意味著它比原來快2倍,4倍甚至18倍。

另外, 它結合了現有的其他瀏覽器的最先進的優化方式。 所以即使它不是並行運行,它依舊是一個非常迅捷的 CSS 引擎。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

但是 CSS 引擎是做什麼的呢?首先,讓我們看看 CSS 引擎是如何融入其他瀏覽器的。然後我們再來看 Quantum CSS 是如何做到更快的。

CSS 引擎的作用是什麼?

CSS 引擎是瀏覽器渲染引擎的一部分。渲染引擎將網站的 HMTL 和 CSS 文件渲染成屏幕上對應的像素。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

每個瀏覽器都有一個渲染引擎。在 Chrome 中它叫做 Blink,在 Edge 中它叫做 EdgeHTML, 在 Safari 中 它叫做 WebKit,在 Firefox 中它叫做 Gecko。

為了轉化這些文件成為像素點,所有的這些渲染引擎都會做這些相同的事情:

  1. 解析這些文件成瀏覽器能夠理解的對象,包括 DOM。在這一點上, DOM 知道這個頁面的結構。它知道元素之間的父子關係。但是它不知道這些元素該是什麼樣子。

  2. 為了弄清楚這些元素究竟該長什麼樣,對於每個 DOM 節點,CSS 引擎會計算出要應用哪些 CSS 規則,然後計算出那個 DOM 節點應用的每個 CSS 屬性的值。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

  1. 計算出每個節點的大小以及它在屏幕上的位置。 對要出現在屏幕上的東西創建它們所屬的盒子。盒子不僅僅代表 DOM 節點,也會有在 DOM 節點內部的盒子,比如文本行。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

  1. 繪製這些不同的盒子,繪製可以發生在不同的層上。我覺得這個有點像過去用洋蔥皮紙上的手繪動畫。這使得瀏覽器可以只切換一個層而不用在其他層上重新繪製。

  1. 把這些不同的繪製的層,應用任何像transform 這樣的合成屬性,然後把他們變成一張圖像。這基本上就像是給這些疊在一起的層拍一張照,這張圖像之後就會被渲染到屏幕上。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

這意味著當渲染引擎開始計算樣式,CSS 引擎有兩個東西:

  • DOM 樹

  • 一張樣式規則的清單

它將遍歷每個 DOM 節點,然後計算出對應 DOM 節點的樣式。對於這部分,它對當前 DOM 節點的每個 CSS 屬性都給予一個值,哪怕樣式表沒有對這個屬性聲明一個值。

我覺得這好比某個人去填一張表單。他需要為每個 DOM 節點都填寫一張表單,然後表單的每個域都要填上最終的答案。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

為了做到這一點,CSS 引擎需要做兩件事:

  • 計算出當前節點需要應用哪些規則 ,又叫做 選擇器匹配

  • 為任何空缺的值填補上父元素的值或者是默認值,又叫做 層疊

選擇器匹配

對於這一步, 我們將任何匹配當前 DOM 節點的規則添加到一個列表,因為可以匹配多個規則,對於同個屬性也可能會有多次聲明。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

另外,瀏覽器本身也會添加一些默認 CSS (稱作 user agent style sheets)。那麼 CSS 引擎怎麼知道要選擇哪個值呢?

這時候特異性規則就出場了。CSS 引擎基本上會創建一個試算表。然後它會基於不同列分出不同的聲明。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

擁有最高特異性的規則將會勝出。所以根據這張表,CSS 引擎會應用上它能應用的值。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

其他的, 我們會用到層疊。

層疊(cascade)

層疊讓 CSS 更易於書寫和維護。因為層疊,你可以在 body 上設置 color 屬性,然後你就知道 p元素和 span 元素以及 li 元素都使用那個顏色 (除非你有更多具體的樣式覆蓋)。

為了做到這點,CSS 引擎會查看樣式表單中空的盒子。如果這個屬性默認是繼承的,那麼 CSS 引擎就會向樹上查找是否有一個祖先節點有值。如果沒有任何祖先節點有這個值,或者這個屬性沒有繼承,那麼這個屬性就會得到一個默認值。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

所以現在這個 DOM 節點所有的樣式都已經計算好了。

旁註: 樣式結構共享

剛剛那個展現給你們的表單其實是有一些曲解的。CSS 有上百個屬性。如果 CSS 引擎保持著每個 DOM 節點的每個屬性,那內存早就不夠用了。

反而, 引擎實際上乾的事情,叫做樣式結構共享。他們將有關聯的數據(比如字體屬性)存到不同的對象上,叫做樣式結構。然後,計算出的樣式只是通過指針指向具體的樣式對象,而不是把所有的屬性都放在相同的對象上。對於每種屬性,都有一個指針指向擁有對應 DOM 節點樣式的值的樣式結構。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

這樣既節省了內存又節省了時間。 擁有相似屬性的節點(比如兄弟節點)只是指向他們相同的結構並共享那些屬性。同時又因為許多屬性都是繼承的,所以的祖先節點可以和任何不指定具有自己重寫屬性的後代節點共享同一個結構。

現在,我們怎麼樣讓它變得更快?

這就是沒有優化過的樣式計算看起來的樣子。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

瀏覽器在樣式計算裡做了很多事情。 這個過程並不只是發送在頁面第一次加載的時候。隨著用戶和頁面的不斷交互,這個過程在不斷地重複,無論是將鼠標懸停在元素之上還是改變 DOM 結構都會觸發樣式的改變

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

這意味著 CSS 樣式計算是實現優化的重要選項。在過去的20年內,瀏覽器一直在嘗試各種的優化策略。Quantum CSS 將來自於不同引擎的各種策略結合在一起,從而創造出一個超級快的新引擎。

那麼現在就讓我們來看一下他們是如何一起發揮作用的。

所有的運行都是並行的

Servo 項目 (也就是 Quantum CSS 的起源) 的內容是使一個實驗性的瀏覽器將頁面上所有不同部分都並行渲染。這意味著什麼呢?

計算機就像人類的大腦。有一個專門用於思考的部分——算數邏輯單元(ALU)。靠近這部分,有一些用於儲存短期記憶——寄存器(register)。他們共同組成了 CPU 。然後還有一些用於儲存長期記憶,也就是 RAM 。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

早期使用這樣的 CPU 的電腦一次只能處理一件事情。但是經過近十年的發展,CPU 已經進化成可以擁有由多個 ALU 和寄存器組合成的核心。這意味著 CPU 可以一次並行處理多件事情。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

Quantum CSS 利用了當今電腦最新的這些特性將不同 DOM 節點的樣式計算分配給不同的核心。

或許這看起來是一件非常簡單的事情,僅僅是將樹的分支分開在不同的核心上處理。因為某些原因,所以實際上卻比想象中的要困難很多。其中之一就是 DOM 樹通常是不平衡的。這意味著可能某個核心的工作量要比其他的核心要多很多。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

為了更平均的分配這些工作,Quantum CSS 使用了一個稱之為工作竊取(work stealing)的技術。處理一個 DOM 節點時,代碼會獲取他的直接子元素,然後將他們分為一個或多個 “工作單元”。然後這些工作單元會被放進一個隊列之中。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

一旦其中一個核心完成了它當前隊列中的任務,那麼他就會從其他的隊列中去尋找新的任務。這意味著我們不必提前遍歷整棵樹去計算他們的平均任務就可以均勻地分配任務。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

在大多數的瀏覽器之中,很難保證這個方法的正確性。並行性是眾所周知的難題,而 CSS 引擎又十分複雜。 恰好它又處於渲染引擎中的另外兩個非常複雜的部分—— DOM 和佈局之間。所以它很容易產生 bug,而且因為並行性所產生的叫做數據競爭的 bug 難以追蹤。我會在 另一篇文章中闡述更多這類 bug。

如果你的程序接受了來自成百上千的工程師的辛勤奉獻,如何讓你的程序不怕在並行環境從運行呢?這就是 Rust 的意義所在。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

有了 Rust, 你就可以靜態地驗證以確保沒有數據競爭。這意味著通過提前防止難以調試的 bug 寫入你的代碼之中,你可以避免這些難以調試的 bug。而編譯器是不會讓你這麼做的。將來我會撰寫更多關於這個內容的文章。與此同時,你可以觀看這個視頻 intro video about parallelism in Rust 或者這個視頻 more in-depth talk about work stealing。

有了這個,CSS 樣式計算變成了一個所謂的尷尬的並行問題——很少有東西會阻止你在並行中更高效地運行。這意味著我們可以得到接近線性的速度提升。假如在你的電腦上有四個核心,那麼它會以接近原來四倍的速度運行。

通過規則樹來加快樣式重置

對於每個 DOM 節點, 都需要CSS 引擎去遍歷所有的規則去實現選擇器匹配。對於大多數的節點,這個匹配很大程度上不會經常發生變化。比如,當用戶把鼠標懸停在一個父元素上,匹配的規則或許會發生變化。但是我們仍然需要為所有的後代元素重新計算樣式來處理屬性繼承,然而匹配規則的後代元素很有可能不會發生任何變化。

如果我們可以為這些匹配到的後代元素這個記錄就好了,這樣我們就不用對他們再進行選擇器匹配了。這就是所謂的規則樹——從 Firefox 的上一代 CSS 引擎 — does 中借來。

CSS 引擎會通過這個過程計算出需要匹配的選擇器,並通過特異性將他們分類出來。通過這個方式,就創建了鏈接的規則列表。

這個列表將會被添加到樹中。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

CSS 引擎會嘗試保存最少分支的樹。為了做到這一點,它會盡量嘗試複用分支。

如果在列表中的大多數選擇器和已有的分支相同,那麼它會沿用同樣的路徑。但是它有可能會遇到這種情況——列表中的下一條規則並不在當前樹的分支中,只有在這種情況下它才會添加一個新的分支。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

DOM 節點會得到指向最後被插入的規則的指針(在這個例子當中,就是 div#warning 規則)。這是最這是最特殊的地方。

關於樣式重置,引擎會做一次快速檢查,去檢查父元素上的改變是否會潛在地改變子元素上匹配的規則。 如果不是,那麼對於任何的後代元素,引擎可以通過後代元素上的指針去獲取那條規則。從這裡,它能夠順著樹回到根節點以獲取完整的規則匹配的列表,從最具體的到最不具體的。這意味著它能夠完全跳過選擇器匹配和排序。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

這個可以大大減少在樣式重置期間的工作。但是在初始化樣式的時候仍然需要很多工作。如果你有10,000 個節點,你仍然需要進行 10,000 選擇器匹配。但是也有其他的方式去加速這個過程。

通過樣式緩存共享加速初始渲染 (以及層疊)

試想一個擁有上千個節點的頁面,許多節點都會匹配同樣的規則。比如,一個很長的維基百科的頁面。 在主內容區域的段落都最終會匹配相同的規則,擁有同樣的計算後的樣式。

如果不進行優化, CSS 引擎就不得不為每個單獨的段落進行選擇器匹配和樣式計算。但是如果有一種方法能夠證明這個樣式在段落與段落之間都是相同的,那麼引擎就可以只做一次運算,並將每個段落節點都指向同樣的計算樣式。

這就是所謂的樣式緩存共享 —— 被 Safari 和 Chrome—does 所啟發。當引擎處理完一個節點時,計算樣式會被放入緩存中。然後,在引擎開始計算下一個節點的樣式之前,它會運行一些檢查,檢測是否有可用的緩存。

這些檢查是:

  • 兩個節點是否擁有相同的 id, 類名, 或者其他?如果是,那麼他們會匹配到相同的規則。

  • 對於所有那些不是基於選擇器的——內聯樣式,引擎會檢查比如,節點是否有相同的值?如果是,那麼先前的規則要麼不被覆蓋要麼以同樣的方式被覆蓋。

  • 節點的父元素是否指向相同的計算樣式對象?如果是,那麼他們的繼承值將會相同。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

從一開始,這些檢查就處於早期的樣式共享緩存中。但是可能仍然會有許多樣式不一定匹配的個例。比如,如果 CSS 規則使用了 :first-child 選擇器,那麼兩個段落就不一定會匹配。即使這些檢查建議它們是匹配的。

在 WebKit 和 Blink 中,這些情況會放棄使用樣式共享緩存。隨著更多的站點使用這些現代選擇器,這種優化策略變得越來越不中用了,所以最近 Blink 團隊已經移除了這個功能。結果卻發現有另外一種方式來使樣式共享緩存能夠跟上這些改變。

在 Quantum CSS 中,我們將這些怪異的選擇器都集中起來然後檢查它們是否在 DOM 節點中使用。然後我們將結果存為 1 和 0。如果兩個元素有相同的 1 和 0,那麼我們就確定了它們是匹配的。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

如果一個 DOM 節點能夠共享已經計算好的樣式,那麼你就可以跳過許多的任務。因為頁面通常都有很多樣式相同的節點,樣式共享緩存便能夠節省內存並真正地加快運行速度。

幾張圖程序員帶你揭祕一個超快的 CSS 引擎

結論

這是的一個從 Servo tech 到 Firefox 的重大技術遷移。一路上,我們學到了如何將寫在 RUST 中的現代的高性能的代碼帶到 Firefox 的核心中。

這裡還是要推薦下我自己建的前端學習群:657137906,如果你正在學習前端,小編歡迎你加入,大家都是前端黨,不定期分享乾貨(只有web前端相關的),包括我自己整理的一份2017最新的前端資料和零基礎入門教程,歡迎初學和進階中的小夥伴。

我們非常高興能夠將 Quantum 這個龐大的項目給用戶帶來第一時間的體驗。我們很高興能讓你嘗試使用,如果你 發現了任何問題請告知我們。

相關推薦

推薦中...