本文是 Chrome 團隊新人入職學習資料《Life of a Pixel》的概要版,首發於個人博客( 點此查看),歡迎關注。
原文 Slides 地址: https://bit.ly/lifeofapixel
中文字幕演講視頻地址: https://www.bilibili.com/vide...
《Life of a Pixel》內容講的是開發者編寫的 web 內容(也就是一般所說的 HTML+CSS+JS 以及 image、video 等其餘資源)渲染爲圖形並呈現到屏幕上的整個過程。我將其演講內容分爲如下三個部分,第一個是靜態渲染過程,講述一個完整的從 content 到 pixel 的渲染過程;第二個是動態更新過程,講述瀏覽器如何高效更新頁面內容。前端
首先看一下整個過程的概覽。在瞭解詳細內容前,咱們也大概知道瀏覽器最終是經過調用 GPU 完成像素到屏幕的繪製。但這個過程當中有不少的步驟。注意概覽圖中瀏覽器的渲染進程是放在沙箱進程中由 Blink 處理的,這也是其安全策略。git
這一頁的內容對於廣大前端從業者來講應該都比較熟悉。首先是 HTML 經過 HTMLDocumentParser 轉換爲 DOM 樹,CSS 經過 CSSParser 轉換爲 StyleRule 集。每一個 StyleRule 包含 CSSSelector 和 CSSPropertyValue,固然兩者間存在對應關係。再加上瀏覽器提供的每種類型元素的 DefaultStyle,通過一系列的計算(這一步稱爲 recalc)生成全部元素包含全部 style 屬性值的 ComputedStyle,如右上角的圖所示。ComputedStyle 也經過開發者工具和 JS API 暴露了出來,相信你們也不陌生。web
接下來一步就是 layout。layout 的功能是根據上一步獲得的全部元素的 computedStyle,將全部元素的位置佈局計算好。每一個元素在這一步會生成一個 LayoutObject,簡單來講其包含四個屬性:x、y、width 及 height 用於標識其佈局位置。layout 最簡單的狀況就是,全部的塊按照 DOM 順序從上往下排列,也就是咱們常說的流。layout 也包含很複雜的狀況,好比帶有 overflow 屬性的元素,瀏覽器會計算其 border-box 的長寬和實際內容的長寬。若是設置爲 scroll 而且內容超出,還要爲其預留滾動條的位置。此外, float、flexbox 等佈局也會使得 layout 變複雜。。因此爲了解決複雜性的問題,layout 階段瀏覽器首先會生成一個和 DOM 樹節點大體一一對應的 layout 樹,而後遍歷該樹,將通過計算後得出的位置佈局數據填入節點。對於這個過程,Chrome 團隊認爲沒有很好地分離輸入和輸出,所以下一代的 layout 系統會進行重構,使得分層更加清晰。api
而後進入 paint 階段。須要注意的是這一步並非真的繪製,只是生成對應的指令。對於每一個 LayoutObject,瀏覽器會生成一個列表,列表中的每一項記錄着繪製指令(好比畫個紅色的矩形)。記住這個待繪製列表項,後面會出現不少次。繪製按照堆棧也就是 z 軸的順序在多個階段進行。每一個階段只根據當前元素對應的屬性(background->floats->foregrounds->outlines)進行繪製。注意,繪製並不嚴格是按照上述四種元素屬性順序,此處只做舉例說明。瀏覽器
下面就進入 raster 階段,中文名爲柵格化。柵格化的操做將上一步 paint 階段每一個 LayoutObject 存儲的繪製指令列表中的每一項轉換爲顏色值的位圖。位圖中的每一項存儲着 RGBA 值,對應着一個像素。位圖存在於 GPU 內存中,尚未顯示到屏幕上。GPU 除了用來存位圖信息,還能執行生成位圖的命令,也就是說柵格化過程可經過 GPU 進行,Chrome 默認開啓 GPU 柵格化。GPU 柵格化的過程以下:瀏覽器調用 Skia 庫,Skia 庫對繪製指令創建單獨的緩衝區以進行指令的轉譯處理,這一過程結束後緩衝區內容被釋放輸出並生成 OpenGL 調用。至此,這些 OpenGL 調用還存在於渲染沙箱進程,須要經過命令緩衝區機制代理傳輸到 GPU 進程執行。使用 GPU 進程的緣由一是須要繞過渲染器沙箱的限制,二是將 OpenGL 程序若是不穩定或有安全漏洞,隔離開使其不至於影響瀏覽器的穩定性。在將來演進上,柵格化處理將轉移至 GPU 進程中進行,以提高性能。同時 Vulkan 也會被支持。(注:Skia 是一個獨立的圖形處理函數庫,其對硬件作了一層抽象,能夠執行一系列相對底層 OpenGL 更復雜的指令。OpenGL 是跨語言跨平臺的系統級繪圖API。Vulkan 是下一代的繪圖 API,旨在替代 OpenGL。)安全
以上過程揭示了靜態渲染,也就是從 web content 到內存中的像素的整個流程。可是實際過程當中頁面是不斷更新的,包括滾動、動畫、js 等都會改變頁面內容。一個完整的渲染過程是很昂貴的,如何高效更新也是討論的重點。ide
首先明確一個概念,幀。涉及到時間時,每一幀是當前 Web 內容的完整呈現,一般,若是每秒低於 60 幀,滾動和動畫就會顯得有些卡頓。函數
第一個優化方向最容易想到,即跟蹤改變的部分,複用沒有改變的部分。所以針對第一部分提到的 style、layout、paint、raster,瀏覽器都作了精細化跟蹤失效的處理,每一幀都會複用前一幀沒有變化的部分,只有被標記了須要變動的部分纔會進行從新處理。工具
因爲 JS 和渲染都存在於主線程中,所以若是 JS 佔據主線程作了耗時的操做,即便渲染很快,頁面看起來仍然是比較卡頓的。因此這又引出了下一個優化點,compositing,中文名合成。佈局
合成包含兩個概念,一是將頁面分解成多個 layer,二是將這些 layer 在另外一個線程中合成。layer 相似 PS 中圖層的概念,能夠獨立於其餘 layer 進行變換和柵格化。開發者工具中對其也有直觀的展現。合成線程須要可以處理用戶可能致使頁面發生變化的輸入事件好比(變換、剪切、滾動、特效),由於這些操做涉及了複合圖層的改變。這樣能夠和主線程執行 js 互不干擾。可是當合成線程沒法處理某個輸入事件時,仍是會由主線程來處理。layer 的存儲依然是經過樹形結構實現。合成更新是新出現的生命週期,出如今 layout 以後 paint 以前。每一個 layer 都被單獨繪製,所以其也有屬於本身的繪製指令列表。將來,Chrome 可能會將合成圖層生命週期放到 paint 後面。
主線程的繪製階段完成後,主線程上的 layer tree 將會被複制到合成線程上,合成完畢後再返回主線程。整個過程相似 git 中分支代碼的合併。
合成線程中,在對圖層進行柵格化以前,還會有一步 tiling 的操做,也就是將 layer 拆分爲多個小圖塊(tile),目的是爲了防止出現某些狀況下,某個滾動 layer 很長,但實際只須要展現當前容器內的一小塊,若是整個 layer 進行柵格化將會比較浪費資源。複雜管理分塊的模塊叫 tile manager,它會隨着滾動區域的變化,優先建立相鄰的圖塊。全部圖塊柵格化完成後,合成線程將繪製 quads(四邊形繪製)。一個 quad 相似於在屏幕上繪製一個圖塊的指令,其引用在內存中生成的柵格圖塊,而後被封裝,由渲染進程提交到瀏覽器進程,這些就是每一個動畫幀。
這裏爲了實現能夠一邊能夠執行前一個提交的圖塊繪製任務,一邊繼續等待新的任務,合成線程還作了一些優化,實現了一個 pending layer tree。其接收 commit,當其準備好繪製後,會被激活(activation)從而複製到 active layer tree 上進行繪製任務。
瀏覽器拿到渲染進程發來的動畫幀以後,結合非內容區的其餘渲染進程(好比瀏覽器 UI),調用 OpenGL 指令繪製最終的畫面。
最後仍是這張圖,快速過一下每一個步驟,web 內容、生成 DOM 樹、解決樣式問題、更新佈局、生成合成圖層、把圖層繪製到待顯示項列表中、把圖層樹提交給合成線程、把圖層切分爲小圖塊、對圖塊進行柵格化操做、把 pending layer tree 複製到 active layer tree、把樹繪製成 quads、提交 quad 到瀏覽器進程、經過 GPU 進程調用 GL 指令繪製像素至屏幕上。
以上,就是一個像素的一輩子奇妙之旅。