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 因為是 HTML 的裝飾,所以自然而然的也是要依附到 DOM 物件上。依附的過程,也就是 CSS 選擇的過程。但是由於 HTML 的樹狀結構,CSS 有時需要寫的十分複雜以去高效的匹配到相對應的 DOM 元素。
在 CSS 解析時,解析器會將每一個選擇器所選擇的 CSS 屬性名和屬性值儲存,作為 map,同時視訊中提及,CSS 屬性名是由 C++ 進行生成的,該 C++ 檔案在構建時由 python 指令碼自動生成。下一步,稱為重計算(recalc),對於所有產生的屬性,我們會計算它們的疊加和,作為每一個 DOM 元素的每一個屬性的值,這個值我們也稱為計算值。也就是最終渲染的結構。這個屬性值我們可以使用 Chrome 的 API 或者是 JavaScript DOM API 均可以得到。
Layout
通過上面獲得的計算屬性,我們就可以確定每一個元素在檢視上佔據的確定位置。這裡舉一個簡單的例子。就是每一個元素是由上向下依次排列的,每一個元素的高度由字型的大小和間距所決定。但是,實際情況往往可能比較複雜。
比如一個元素的內容超出了邊界。那麼該元素會呈現可滾動的特性,這時頁面需要實時計算顯示的區域。再比如表格佈局,浮動,文字分列,flex 佈局,writing-mode等屬性,都會帶來佈局的複雜程度。所以,我們需要一個更加完善的資料結構去儲存這樣一些狀態,使得每一次迭代都能夠變得高效和純粹。
這裡引入了新的資料結構,也是對於之前重計算所得到的 Render Tree 的進一步封裝,將之前的複雜情況進一步考慮,得到最終每一個元素在檢視上的最終位置,即 Layout Tree。需要注意的是,不是每一個 DOM 元素都對應有一個 Layout Object,比如對於一個 display 屬性設定為 none 的元素。同時,也不是每一個 Layout Object 都對應於一個 DOM 元素,比如偽元素。
基於 Layout Tree,我們就可以處理 overflow 等一系列複雜情況。但還有一個個問題是,這種資料結構沒有將輸入的計算屬性和輸出的檢視位置分離開,所以這裡提及了一個正在開發的新專案名為 LayoutNG 就是為了解決這個問題。
Paint
得到了上個步驟的 Layout 物件也就意味著我們可以真正的繪製畫素了。但注意,這裡的繪製也僅僅是語義上的,並沒有落實到螢幕上。我們會再一次將 Layout 物件轉化為一個一個矩形和其對於的顏色,並且將其按堆疊的形式顯示,並非是 DOM 的出現順序。並且每一次渲染都是根據某一種屬性,比如 PPT 中簡化的幾種,背景,浮動,前景,輪廓。在示例中雖然帶有 blue class 的元素在 green class 之後,可是 green class 中的文字卻顯示在最前,原因也就是 foreground 屬性在檢視的位置上,要先顯示。
Raster
這裡我們得到了元素最終在檢視的顯示資訊,我們就需要真正的進行預渲染。在這一步,我們會將螢幕上每一個畫素點的顏色生成為一個 32 位的二進位制碼,分別表示 4 中顏色,同時我們可以利用 GPU 進行渲染加速,這裡還要提及的是使用 Google 自主開發的 skia 專案進行影像處理渲染,通過最底層的 OpenGL。同時 skia 作為一個單獨的專案,也支援了其他的大型專案,比如安卓系統。
gpu
最後需要注意的是真正再通過 skia 呼叫的 openGL API 會通過 CommandBuffer 呼叫 GPU 程式,進行真實的渲染。也就是說,真實的渲染是獨立出去的,當這一部分程式當機時我們可以快速的重啟。同時之前也提到過,這裡我們會將 openGL 轉化為 DirectX 在 Windows 平臺上,通過 Angle 這個庫。當然,Vulkan 的開發就是為了統一,同時,開發者也在嘗試著將 skia 呼叫這個模組也放入 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,它負責將區域分塊,就像地板上的瓦塊一樣,隨著滾動區域的變化,將相鄰區域的瓦塊優先渲染。
所有主要的階段已經大體介紹完畢。歡迎補充和加深!
感謝張冀韜同學將演講內容梳理成文章並於掘金首發。