Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

已禁用發表於2018-09-17

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

大體流程

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

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

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

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

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

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

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

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

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

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

初始渲染

parse(解析)

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

DOM

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

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

Style

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素
CSS 因為是 HTML 的裝飾,所以自然而然的也是要依附到 DOM 物件上。依附的過程,也就是 CSS 選擇的過程。但是由於 HTML 的樹狀結構,CSS 有時需要寫的十分複雜以去高效的匹配到相對應的 DOM 元素。

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

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

Layout

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素
通過上面獲得的計算屬性,我們就可以確定每一個元素在檢視上佔據的確定位置。這裡舉一個簡單的例子。就是每一個元素是由上向下依次排列的,每一個元素的高度由字型的大小和間距所決定。但是,實際情況往往可能比較複雜。

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素
比如一個元素的內容超出了邊界。那麼該元素會呈現可滾動的特性,這時頁面需要實時計算顯示的區域。再比如表格佈局,浮動,文字分列,flex 佈局,writing-mode等屬性,都會帶來佈局的複雜程度。所以,我們需要一個更加完善的資料結構去儲存這樣一些狀態,使得每一次迭代都能夠變得高效和純粹。

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素
這裡引入了新的資料結構,也是對於之前重計算所得到的 Render Tree 的進一步封裝,將之前的複雜情況進一步考慮,得到最終每一個元素在檢視上的最終位置,即 Layout Tree。需要注意的是,不是每一個 DOM 元素都對應有一個 Layout Object,比如對於一個 display 屬性設定為 none 的元素。同時,也不是每一個 Layout Object 都對應於一個 DOM 元素,比如偽元素。

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

Paint

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素
Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素
得到了上個步驟的 Layout 物件也就意味著我們可以真正的繪製畫素了。但注意,這裡的繪製也僅僅是語義上的,並沒有落實到螢幕上。我們會再一次將 Layout 物件轉化為一個一個矩形和其對於的顏色,並且將其按堆疊的形式顯示,並非是 DOM 的出現順序。並且每一次渲染都是根據某一種屬性,比如 PPT 中簡化的幾種,背景,浮動,前景,輪廓。在示例中雖然帶有 blue class 的元素在 green class 之後,可是 green class 中的文字卻顯示在最前,原因也就是 foreground 屬性在檢視的位置上,要先顯示。

Raster

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素
這裡我們得到了元素最終在檢視的顯示資訊,我們就需要真正的進行預渲染。在這一步,我們會將螢幕上每一個畫素點的顏色生成為一個 32 位的二進位制碼,分別表示 4 中顏色,同時我們可以利用 GPU 進行渲染加速,這裡還要提及的是使用 Google 自主開發的 skia 專案進行影象處理渲染,通過最底層的 OpenGL。同時 skia 作為一個單獨的專案,也支援了其他的大型專案,比如安卓系統。

gpu

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素
最後需要注意的是真正再通過 skia 呼叫的 openGL API 會通過 CommandBuffer 呼叫 GPU 程式,進行真實的渲染。也就是說,真實的渲染是獨立出去的,當這一部分程式當機時我們可以快速的重啟。同時之前也提到過,這裡我們會將 openGL 轉化為 DirectX 在 Windows 平臺上,通過 Angle 這個庫。當然,Vulkan 的開發就是為了統一,同時,開發者也在嘗試著將 skia 呼叫這個模組也放入 GPU 的程式。

總結

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

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

更新渲染

引入

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

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

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

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

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

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

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

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

解決方案 compositing

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

基於之前提到的種種問題,Chromium 團隊提出了 compositing 這種解決方案。目的就是優化效能。有點類似於 Photoshop,簡單得說,有兩點:

  1. 頁面分成獨立的層,每一層之間的渲染是獨立的
  2. 單獨使用一個執行緒(impl)去渲染層

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

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

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

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

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

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

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

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

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

Life of a Pixel:前端程式碼如何通過瀏覽器演化為螢幕顯示的畫素

所有主要的階段已經大體介紹完畢。歡迎補充和加深!

感謝張冀韜同學將演講內容梳理成文章並於掘金首發。

相關文章