Life of a Pixel 本來是 Chromium 團隊在入職培訓時的培訓資料,其目的是為了讓新入職的同事能夠從大體上快速的瞭解 Chromium 的架構,而不是糾結於程式碼邏輯。現在該團隊正式將其釋出,也是為了對於此感興趣的工程師能夠快速的瞭解專案,參與專案的開發協作。本視訊的內容,從巨集觀上來說,就是本演講的題目 Life of a Pixel,直譯就是一個畫素點的一生,表示該演講作者希望觀眾能夠在視訊結束後瞭解,前端的工程師所完成的程式碼,是如何通過瀏覽器,變為一個又一個的畫素點,以及畫素點是如何更新和毀滅的。
大體流程

web content (程式碼) ➡️ magic (渲染) ➡️pixels (畫素)

- HTML(Hyper-Text Markup Language, 頁面結構與內容)
- CSS(Cascading Style Sheets, 頁面樣式)
- JS(JavaScript, 負責頁面結構與內容和頁面樣式的更新)

瀏覽器真正渲染的內容在紅框內,之外的都是非渲染的部分。渲染的引擎可以看做是一個黑箱,在 Chromium 中,我們把它稱為 Blink。

同時,在渲染時,我們需要呼叫影象處理的底層 API 去進行渲染,對於此,有官方的統一標準就是 openGL,但是,對於 Windows,可能還需要轉化為 DirectX。對於此,團隊正在開發一個新專案名為 Vulkan,為了進行統一化。當然,這種底層的 API 並不能讀懂我們的 HTML 和 CSS,它們只能做一些簡單得影象繪製,諸如畫一些多邊形這種操作。


所以,我們再梳理一遍流程。總的來說就是我們要將 web content 轉化為對於的影象處理的 API,在電腦螢幕上進行繪製。在這個過程中,為了更好地將已經渲染的影象更新,我們要設計一種資料結構,能夠幫助我們更新這個頁面的結構與內容和頁面樣式。這些更新就包括我們熟知的 JavaScript API,使用者在輸入框輸入,非同步載入,動畫,卷軸移動,頁面縮放。
初始渲染
parse(解析)

DOM


HTML 的結構,是一種天然的語義化的繼承式的結構。語義化是標籤所帶來的,整合式是樹狀結構所帶來的。我們可以將 HTML 看做輸入進行解析,成為一顆我們熟知的 DOM 樹。很好的詮釋了父子間,兄弟間的關係。我們也可以很直觀的從 JavaScript 所暴露出的 DOM API 中發現。
Style






在 CSS 解析時,解析器會將每一個選擇器所選擇的 CSS 屬性名和屬性值儲存,作為 map,同時視訊中提及,CSS 屬性名是由 C++ 進行生成的,該 C++ 檔案在構建時由 python 指令碼自動生成。下一步,稱為重計算(recalc),對於所有產生的屬性,我們會計算它們的疊加和,作為每一個 DOM 元素的每一個屬性的值,這個值我們也稱為計算值。也就是最終渲染的結構。這個屬性值我們可以使用 Chrome 的 API 或者是 JavaScript DOM API 均可以得到。
Layout








基於 Layout Tree,我們就可以處理 overflow 等一系列複雜情況。但還有一個個問題是,這種資料結構沒有將輸入的計算屬性和輸出的檢視位置分離開,所以這裡提及了一個正在開發的新專案名為 LayoutNG 就是為了解決這個問題。
Paint



Raster



gpu



總結

我們假設初次渲染已經完成,但是,對於前端的快速發展,大量的邏輯已經由後端轉往前端實現,DOM 的更新變得異常頻繁。簡單地說,我們需要在原有 DOM 上做適量的改動重新渲染。為了不重新將上圖的整個流程全部再次進行,這裡我們就需要將其中的某些狀態保留,提高更新效率。
更新渲染
引入

在更新渲染時,有時我們會縮放頁面,區域滾動,或者是有動畫。在這型別的情況下,如果渲染速率低於60幀,那麼人眼看到會變得有些卡頓。

所以我們要儘可能判斷出在上節提到的每一個步驟中,有哪些元素是需要改變的,哪些不需要是可以重新利用的,做到效率的優化。這也是在技術實現中也被考慮到的地方。

但是,實際情況是,有時一個大的區域全部改變,那麼我們不得不對這個大的區域進行全部重新渲染,比如區域滾動。

還要注意的是 JavaScript 的設計是單執行緒的,也就意味著在渲染時,加入有 JS 指令碼的執行,就會阻塞當前的渲染。
解決方案 compositing

基於之前提到的種種問題,Chromium 團隊提出了 compositing 這種解決方案。目的就是優化效能。有點類似於 Photoshop,簡單得說,有兩點:
- 頁面分成獨立的層,每一層之間的渲染是獨立的
- 單獨使用一個執行緒(impl)去渲染層


在我們進行動畫,滾動,縮放等操作時,瀏覽器會監聽使用者的輸入行為,在 impl 執行緒上進行工作,使得主執行緒執行 JavaScript,互不干擾,但是假如 impl 執行緒發現這個事件無法處理,則還是會交還給主執行緒。



在實現層這個概念時還是會借鑑初次渲染的資料結構,也就是樹,稱為 Layer Tree。它是命名在 cc(Chromium compositor)下,主要資料資訊由之前的 Layout Tree 繼承而來。注意,這裡還有一個 PaintLayer Tree, 類似於一箇中間狀態,將一個 Layout Object 進行分層,並且賦予其功能,例如對子元素進行裁切或者是施加別的效果。



自然而然,我們將會在 layout 和 paint 這兩個階段中加入 compositing update 去加快大區域重新渲染,獲得 layer tree。需要注意的是,現在團隊中正在進行一個工程,稱為 slimming paint,將 layer tree 的建立放在 paint 階段後,目的是為了將每一層 layer 的建立變得更加獨立,並且建立屬性樹,提取出獨立或者公共的屬性,儘可能地將其放到真正畫素級渲染之前。當 impl 執行緒的 paint 階段結束後,就可以通知主執行緒進行同步,有點類似於使用 git 在不同分支上合併程式碼。

在 raster 之前還有一步優化,對於大面積滾動檢視,沒有必要一開始將所有的內容全部變換成 bitmaps,我們只需要將視窗中的先進行轉化,在這裡有一個 tiling manager,它負責將區域分塊,就像地板上的瓦塊一樣,隨著滾動區域的變化,將相鄰區域的瓦塊優先渲染。




所有主要的階段已經大體介紹完畢。歡迎補充和加深!
感謝張冀韜同學將演講內容梳理成文章並於掘金首發。