瀏覽器渲染詳細過程:重繪、重排和 composite 只是冰山一角

在以前的一篇文章中:Vue源碼詳解之nextTick:MutationObserver只是浮雲,microtask纔是核心!,我說過,偶然在一次對task和microtask的討論當中,研究到了瀏覽器在處理完task和microtask以後執行的渲染機制,當時看到這個內容,仍是挺激動的,由於之前歷來不知道我在js裏更改的樣式,瀏覽器究竟是何時、以怎樣的方式渲染到界面上的,因而興奮的寫下了上述文章。javascript

最近深刻研究了這部分,發現這裏有一片更廣闊的新大陸,在咱們耳熟能詳的重排、重繪、composite、合成層提高等概念下還有更深的的東西。這篇文章將會介紹瀏覽器的詳細渲染過程。css

event loop規範和處理過程

這裏我在開頭說的nexttick詳解中說過這部分,可是隻是本身看嗨了,並無做爲文章重點去詳細介紹,當時主要仍是說task和microtask。其實這部分是整個渲染過程的關鍵,涉及瀏覽器進行渲染的時機,因此認真說一下:html

請先點連接看html5官方規範: html5 event loop processing modelhtml5

  1. 前1到5步,從多個task隊列中裏選出一個task隊列(瀏覽器爲了區分不一樣task的優先級,因此時常有多個task隊列),從這個task隊列中取出最老的那個task,執行他,而後把他從隊列中去除。
  2. 第六步perform a microtask checkpoint執行一個microtask檢查點,這個步驟其實包含了多個子步驟,只要microtask queue不空,這一步會一直從microtask queue中取出microtask,執行之。若是microtask執行過程當中又添加了microtask,那麼仍然會執行新添加的microtask。( 連接裏第七步的return to the microtask queue handling step很關鍵)
  3. 第七步Update the rendering,更新渲染。到了更新界面的部分了!
    1. 7.1步到7.4步,判斷當前的document是否須要渲染,用官方規範的說法就是瀏覽器會判斷這個document是否會從UI Render中獲益,由於只須要保持60Hz的刷新率便可,而每輪event loop都是很是快的,因此不必每輪loop都Render UI,而是差很少16ms的時候再Render。同時對於一些比較卡頓的已經不能保證60Hz的頁面,若再在此時執行界面渲染會雪上加霜,因此瀏覽器可能會下調認爲document能獲益的頻率爲(好比說)30hz。
    2. run the resize steps,若瀏覽器resize過,那麼這裏會在Window上觸發’resize’事件。
    3. run the scroll steps。首先,每當咱們在某個target上滾動時(target能夠是某個可滾動元素也可能就是document),瀏覽器就會在target所屬的document上的pending scroll event targets裏存放這個發生滾動的target。如今, run the scroll steps這一步會從pending scroll event targets裏取出target,而後在target上觸發scroll事件。
    4. 計算是否觸發media query
    5. 7.8和7.9 執行css animation和觸發‘animationstart’等animation相關事件. run the fullscreen rendering steps:若是在以前的task或者microtask中執行過 requestFullscreen()等full screen相關api,此處會執行全屏操做。
    6. run the animation frame callbacks,執行requestAnimationFrame的回調,requestAnimationFrame就是在這裏執行的!
    7. 執行IntersectionObserver的回調,也許你在圖片懶加載的邏輯裏用過這個api。
    8. 更新、渲染用戶界面
  4. 繼續返回第一步

好了,整個流程就介紹完了。前兩步task和microtask相關處理再也不贅述。
重要的第三步裏有3點值得關注的東西:java

  1. 不是每輪event loop裏都會有Update the rendering,只有瀏覽器判斷這個document須要更新界面的時候纔會讓更新。這意味着兩次UI Render之間最小間隔也是16ms,你setInterval去1ms更新一次其實也依然是16ms更新一次。
  2. resize和scroll事件是在渲染流程裏觸發的。是否是很驚人?這意味着若是你想在scroll事件上綁回調去執行動畫,那麼根本不須要用requestAnimationFrame去節流,scroll事件自己就是在每幀真正渲染前執行,自帶節流效果!固然,滾動圖片懶加載、滾動內容無限加載等業務邏輯而非動畫邏輯仍是須要throttle的。
  3. 如同mdn、w3c等介紹的:requestAnimationFrame的回調是在重繪前執行的,7.9步是這一邏輯的保證。
  4. UI的重繪是在event loop的結束時執行的。
    頁面的重繪居然是跟event loop緊密耦合的,並且是被精肯定義在event loop中的,始料未及。我以前一直不明白我JS修改了DOM樣式以後,這樣式到底何時呈現。

頁面渲染的時機介紹完了,來講說渲染究竟是怎樣一個過程。另,後文講述的是瀏覽器詳細過程,是實現,前文講的是規範css3

渲染

這張很經典的圖許多人都看過,其中的概念你們應該都很熟悉,也就是這麼幾個步驟:
js修改dom結構或樣式 -> 計算style -> layout(重排) -> paint(重繪) -> composite(合成)git

可是其中有更復雜的內容,咱們從更底層來詳細說明這個過程,主要是下面這兩幅圖:

上圖出自GPU Accelerated Compositing in Chrome

上圖出自The Anatomy of a Framegithub

這部份內容基於blink、webkit內核,可是其中涉及到的重排、重繪、composite和合成層提高等環節對於各大瀏覽器都是一致的。web

先說一些概念

  1. 位圖

    就是數據結構裏常說的位圖。你想在繪製出一個圖片,你應該怎麼作,顯然首先是把這個圖片表示爲一種計算機能理解的數據結構:用一個二維數組,數組的每一個元素記錄這個圖片中的每個像素的具體顏色。因此瀏覽器能夠用位圖來記錄他想在某個區域繪製的內容,繪製的過程也就是往數組中具體的下標裏填寫像素而已。chrome

  2. 紋理
    紋理其實就是GPU中的位圖,存儲在GPU video RAM中。前面說的位圖裏的元素存什麼你本身定義好就行,是用3字節存256位rgb仍是1個bit存黑白你本身定義便可,可是紋理是GPU專用的,GPU和CPU是分離的,須要有固定格式,便於兼容與處理。因此一方面紋理的格式比較固定,如R5G6B五、A4R4G4B4等像素格式, 另一方面GPU 對紋理的大小有限制,好比長/寬必須是2的冪次方,最大不能超過2048或者4096等。

  3. Rasterize(光柵化)


    在紋理裏填充像素不是那麼簡單的本身去遍歷位圖裏的每一個元素而後填寫這個像素的顏色的。就像前面兩幅圖。光柵化的本質是座標變換、幾何離散化,而後再填充
    同時,光柵化從早期的 Full-screen Rasterization基本都進化到了如今的Tile-Based Rasterization, 也就是否是對整個圖像作光柵化,而是把圖像分塊(tile,亦有翻譯爲瓦片、貼片、瓷片…)後,再對每一個tile單獨光柵化。光柵化好了將像素填充進紋理,再將紋理上傳至GPU。
    緣由一方面如上文所說,紋理大小有限制,即便你整屏光柵化也是要填進小塊小塊的紋理中,不如事先根據紋理大小分塊光柵化後再填充進紋理裏。另外一方面是爲了減小內存佔用(整屏光柵化意味着須要準備更大的buffer空間)和下降整體延遲(分塊柵格化意味着能夠多線程並行處理)。
    看到下圖中藍色的那些青色的矩形了嗎?他們就是tiles。

能夠想見瀏覽器的一次繪製過程就是先把想繪製的內容如文字、背景、邊框等經過分塊Rasterize繪製到不少紋理裏,再把紋理上傳到gpu的存儲空間裏,gpu把紋理繪製到屏幕上。

繪製的具體過程

咱們先把計算樣式、重排等步驟抽離,單獨講解瀏覽器是怎麼繪製的。

先來看這幅經典的圖:

圖中一些名詞的稱呼發生了變化,詳見taobaofed的文章:無線性能優化:Composite

Render Object

首先咱們有DOM樹,可是DOM樹裏面的DOM是供給JS/HTML/CSS用的,並不能直接拿過來在頁面或者位圖裏繪製。所以瀏覽器內部實現了Render Object

每一個Render Object和DOM節點一一對應。Render Object上實現了將其對應的DOM節點繪製進位圖的方法,負責繪製這個DOM節點的可見內容如背景、邊框、文字內容等等。同時Render Object也是存放在一個樹形結構中的。

既然實現了繪製每一個DOM節點的方法,那是否是能夠開闢一段位圖空間,而後DFS遍歷這個新的Render Object樹而後執行每一個Render Object的繪製方法就能夠將DOM繪製進位圖了?就像「蓋章」同樣,把每一個Render Object的內容一個個的蓋到紙上(類比於此時的位圖)是否是就完成了繪製。

不,瀏覽器還有個層疊上下文。就是決定元素間相互覆蓋關係(好比z-index)的東西。這使得文檔流中位置靠前位置的元素有可能覆蓋靠後的元素。上述DFS過程只能無腦讓文檔流靠後的元素覆蓋前面元素。

所以,有了Render Layer。

Render Layer

固然Render Layer的出現並非簡單由於層疊上下文等,好比opacity小於一、好比存在mask等等須要先繪製好內容再對繪製出來的內容作一些統一處理的css效果。

總之就是有層疊、半透明等等狀況的元素(具體哪些狀況請參考無線性能優化:Composite)就會從Render Object提高爲Render Layer。不提高爲Render Layer的Render Object從屬於其父級元素中最近的那個Render Layer。固然根元素HTML本身要提高爲Render Layer。

所以如今Render Object樹就變成了Render Layer樹,每一個Render Layer又包含了屬於本身layer的Render Object。

另外:

The children of each RenderLayer are kept into two sorted lists both sorted in ascending order, the negZOrderList containing child layers with negative z-indices (and hence layers that go below the current layer) and the posZOrderList contain child layers with positive z-indices (layers that go above the current layer).
每一個Render Layer的子Render Layer都是按照升序排列存儲在兩個有序列表當中的:negZOrderList存儲了負z-indicices的子layers,posZOrderList存儲了正z-indicies的子layers。
— 出自GPU加速的compositing一文

如今瀏覽器渲染引擎遍歷 Layer 樹,訪問每個 RenderLayer,而後遞歸遍歷negZOrderList裏的layer、本身的RenderObject、再遞歸遍歷posZOrderList裏的layer。就能夠將一顆 Layer樹繪製出來。

Layer 樹決定了網頁繪製的層次順序,而從屬於 RenderLayer 的 RenderObject 決定了這個 Layer 的內容,全部的 RenderLayer 和 RenderObject 一塊兒就決定了網頁在屏幕上最終呈現出來的內容。

層疊上下文、半透明、mask等等問題經過Render Layer解決了。那麼如今:
開闢一個位圖空間->不斷的繪製Render Layer、覆蓋掉較低的Layer->拿給GPU顯示出來 是否是就徹底ok了?

不。還有GraphicsLayers和Graphics Context

Graphics Layer(又稱Compositing Layer)和Graphics Context

上面的過程能夠搞定繪製過程。可是瀏覽器裏面常常有動畫、video、canvas、3d的css等東西。這意味着頁面在有這些元素時,頁面顯示會常常變更,也就意味着位圖會常常變更。每秒60幀的動效裏,每次變更都重繪整個位圖是很恐怖的性能開銷。

所以瀏覽器爲了優化這一過程。引出了Graphics Layers和Graphics Context,前者就是咱們常說的合成層(Compositing Layer)

某些具備CSS3的3D transform的元素、在opacity、transform屬性上具備動畫的元素、硬件加速的canvas和video等等,這些元素在上一步會提高爲Render Layer,而如今他們會提高爲合成層Graphics Layer(你若是查看了前文我給的連接,你當時可能會疑惑爲何這些狀況也能提高爲Render Layer,如今你應該明白了,他們是爲提高爲Graphics Layer準備的)。每一個Render Layer都屬於他祖先中最近的那個Graphics Layer。固然根元素HTML本身要提高爲Graphics Layer。

Render Layer提高爲Graphics Layer的狀況:

  • 3D 或透視變換(perspective、transform) CSS 屬性
  • 使用加速視頻解碼的 元素
  • 擁有 3D (WebGL) 上下文或加速的 2D 上下文的 元素
  • 混合插件(如 Flash)
  • 對 opacity、transform、fliter、backdropfilter 應用了 animation 或者 transition(須要是 active 的 animation 或者 transition,當 animation 或者 transition 效果未開始或結束後,提高合成層也會失效)
  • will-change 設置爲 opacity、transform、top、left、bottom、right(其中 top、left 等須要設置明確的定位屬性,如 relative 等)
  • 擁有加速 CSS 過濾器的元素
  • 元素有一個 z-index 較低且包含一個複合層的兄弟元素(換句話說就是該元素在複合層上面渲染)
  • ….. 全部狀況的詳細列表參見淘寶fed文章:無線性能優化:Composite

3D transform、will-change設置爲 opacity、transform等 以及 包含opacity、transform的CSS過渡和動畫 這3個常常遇到的提高合成層的狀況請重點記住。

另外除了上述直接致使Render Layer提高爲Graphics Layer,還有下面這種由於B被提高,致使A也被隱式提高的狀況,詳見此文: GPU Animation: Doing It Right

每一個合成層Graphics Layer 都擁有一個 Graphics Context,Graphics Context 會爲該Layer開闢一段位圖,也就意味着每一個Graphics Layer都擁有一個位圖。Graphics Layer負責將本身的Render Layer及其子代所包含的Render Object繪製到位圖裏。而後將位圖做爲紋理交給GPU。因此如今GPU收到了HTML元素的Graphics Layer的紋理,也可能還收到某些由於有3d transform之類屬性而提高爲Graphics Layer的元素的紋理。

如今GPU須要對多層紋理進行合成(composite),同時GPU在紋理合成時對於每一層紋理均可以指定不一樣的合成參數,從而實現對紋理進行transform、mask、opacity等等操做以後再合成,並且GPU對於這個過程是底層硬件加速的,性能很好。最終,紋理合成爲一幅內容最終draw到屏幕上。

因此在元素存在transform、opacity等屬性的css animation或者css transition時,動畫處理會很高效,這些屬性在動畫中不須要重繪,只須要從新合成便可。

上述分層後合併的過程能夠用一張圖來描述:

繪製的具體實現

系統結構

進程

blink和webkit引擎內部都是使用了兩個進程來搞定JS執行、頁面渲染之類的核心任務。

  • Renderer進程
    主要的那個進程,每一個tab一個。負責執行JS和頁面渲染。包含3個線程:Compositor Thread、Tile Worker、Main thread,後文會介紹這三個線程。
  • GPU進程
    整個瀏覽器共用一個。主要是負責把Renderer進程中繪製好的tile位圖做爲紋理上傳至GPU,並調用GPU的相關方法把紋理draw到屏幕上(通常的介紹瀏覽器渲染引擎的文章裏都用paint這個詞表述把內容光柵化和繪製到位圖裏,而用draw這個詞表示GPU最終把紋理顯示到屏幕上),因此這個CPU裏的進程更應該稱爲「負責跟GPU打交道的進程」,不要像我以前同樣由於不懂GPU覺得是GPU裏的一個進程, mdzz。GPU進程裏只有一個線程:GPU Thread。

Renderer進程的三個線程

  • Compositor Thread
    這個線程既負責接收瀏覽器傳來的垂直同步信號(Vsync,水平同步表示畫出一行屏幕線,垂直同步就表示從屏幕頂部到底部的繪製已經完成,指示着前一幀的結束,和新一幀的開始), 也負責接收OS傳來的用戶交互,好比滾動、輸入、點擊、鼠標移動等等。
    若是可能,Compositor Thread會直接負責處理這些輸入,而後轉換爲對layer的位移和處理,並將新的幀直接commit到GPU Thread,從而直接輸出新的頁面。不然,好比你在滾動、輸入事件等等上註冊了回調,又或者當前頁面中有動畫等狀況,那麼這個時候Compositor Thread便會喚醒Main Thread,讓後者去執行JS、完成重繪、重排等過程,產出新的紋理,而後Compositor Thread再進行相關紋理的commit至GPU Thread,完成輸出。
  • Main Thread

    這裏你們就很熟悉了,chrome devtools的Timeline裏Main那一欄顯示的內容就是Main Thread完成的相關任務:某段JS的執行、Recalculate Style、Update Layer Tree、Paint、Composite Layers等等。
  • Compositor Tile Worker(s)
    可能有一個或多個線程,好比PC端的chrome是2個或4個,安卓和safari爲1個或2個不等。是由Compositor Thread建立的,專門用來處理tile的Rasterization(前文說過的光柵化)。

能夠看到Compositor Thread是一個很核心的東西,後面的倆線程都是由他主要進行控制的。
同時,用戶輸入是直接進入Compositor Thread的,一方面在那些不須要執行JS或者沒有CSS動畫、不重繪等的場景時,能夠直接對用戶輸入進行處理和響應,而Main Thread是有很複雜的任務流程的。這使得瀏覽器能夠快速響應用戶的滾動、打字等等輸入,徹底不用進主線程。這裏也有一個很是重要的點,後文會說。
再者,即便你註冊了UI交互的回調,進了主線程,或者主線程很卡,可是由於Compositor Thread在他外面攔着,因此Compositor Thread依然能夠直接負責將下一幀輸出到頁面上,所以即便你的主線程可能執行着高耗任務,超過16ms,可是你在滾動頁面時瀏覽器仍是能作出響應的(同步AJAX等特殊任務除外),因此好比你有一個比較卡的動畫(動畫的預先計算過程或者重繪過程超過16ms每幀),可是你滾動頁面是很是流暢的,也就是動畫卡而滾動不卡(隨便給你個demo本身試試看)。

具體流程

通常咱們在devtools的Timeline裏大概會看到以下過程:

也就是JS執行後觸發重繪重排等操做。這裏着重分析背後的運行過程,即下面這副圖:

圖裏後半部分有兩處commit,分別是主線程通知Main Thread能夠執行光柵化了,以及光柵化完成、紋理生成完畢,Compositor Thread通知GPU Thread能夠將紋理按照指定的參數draw到屏幕上。

總體流程:

  1. Vsync
    接收到Vsync信號,這一幀開始
  2. Input event handlers
    以前Compositor Thread接收到的用戶UI交互輸入在這一刻會被傳入給主線程,觸發相關event的回調。

    All input event handlers (touchmove, scroll, click) should fire first, once per frame, but that’s not necessarily the case; a scheduler makes best-effort attempts, the success of which varies between Operating Systems.

    這意味着,儘管Compositor Thread能在16ms內接收到OS傳來的屢次輸入,可是觸發相應事件、傳入到主線程被JS感知倒是每幀一次,甚至可能低於每幀一次。也就是說touchmove、mousemove等事件最快也就每幀執行一次,因此自帶了相對於動畫的節流效果!若是你的主線程有動畫之類的卡了一點,事件觸發頻率很是可能低於16ms。我在最開始關於渲染時機的內容中說了scroll和resize由於和渲染處於同一輪次,因此最快也就每幀執行一次,如今來看,不只僅是scroll和resize!連touchmove、mousemove等事件,因爲Compositor Thread的機制緣由,也依然如此
    詳見這個jsfiddle,你們能夠試試,你能夠發現mousemove回調和requestAnimationFrame回調的調用頻率是徹底一致的,mousemove的執行次數跟raf執行次數如出一轍,永遠沒有任何一次出現mousemove執行兩次而rAF尚未執行一次的狀況發生。另外兩次執行間隔在14到20毫秒之間,主要是由於幀的間隔不會精確到16.666毫秒哈,基本是14ms~20ms之間大體波動的,你們能夠打開timeline觀察。另外有個挺奇怪的現象是每次鼠標從devtool移回頁面區域裏的時候,會很是快的觸發兩次mousemove(間隔有時小於5ms),雖然依然每次mousemove後依然緊跟raf,這意味着很是快速的觸發了兩幀。

  3. requestAnimationFrame
    圖中的紅線的意思是你可能會在JS裏Force Layout,也就是咱們說的訪問了scrollWidth、clientHeight、ComputedStyle等觸發了強制重排,致使Recalc Styles和Layout前移到代碼執行過程中。
  4. parse HTML
    若是有DOM變更,那麼會有解析DOM的這一過程。
  5. Recalc Styles
    若是你在JS執行過程當中修改了樣式或者改動了DOM,那麼便會執行這一步,從新計算指定元素及其子元素的樣式。
  6. Layout
    咱們常說的重排reflow。若是有涉及元素位置信息的DOM改動或者樣式改動,那麼瀏覽器會從新計算全部元素的位置、尺寸信息。而單純修改color、background等等則不會觸發重排。詳見css-triggers
  7. update layer tree
    這一步實際是更新Render Layer的層疊排序關係,也就是咱們以前說的爲了搞定層疊上下文搞出的那個東西,由於以前更新了相關樣式信息和重排,因此層疊狀況也可能變更。
  8. Paint
    其實Paint有兩步,第一步是記錄要執行哪些繪畫調用,第二步纔是執行這些繪畫調用。第一步只是把所須要進行的操做記錄序列化進一個叫作SkPicture的數據結構裏:

    The SkPicture is a serializable data structure that can capture and then later replay commands, similar to a display list.

    這個SkPicture其實就一個列表,記錄了你的commands。接下來的第二步裏會將SkPicture中的操做replay出來,這裏纔是將這些操做真正執行:光柵化和填充進位圖。主線程中和咱們在Timeline中看到的這個Paint實際上是Paint的第一步操做。第二步是後續的Rasterize步驟(見後文)。

  9. Composite
    主線程裏的這一步會計算出每一個Graphics Layers的合成時所須要的data,包括位移(Translation)、縮放(Scale)、旋轉(Rotation)、Alpha 混合等操做的參數,並把這些內容傳給Compositor Thread,而後就是圖中咱們看到的第一個commit:Main Thread告訴Compositor Thread,我搞定了,你接手吧。而後主線程此時會去執行requestIdleCallback。這一步並無真正對Graphics Layers完成位圖的composite。
  10. Raster Scheduled and Rasterize
    第8步生成的SkPicture records在這個階段被執行。

    SkPicture records on the compositor thread get turned into bitmaps on the GPU in one of two ways: either painted by Skia’s software rasterizer into a bitmap and uploaded to the GPU as a texture, or painted by Skia’s OpenGL backend (Ganesh) directly into textures on the GPU.

    能夠看出Rasterization其實有兩種形式:

    • 一種是基於CPU、使用Skia庫的Software Rasterization,首先繪製進位圖裏,而後再做爲紋理上傳至GPU。這一方式中,Compositor Thread會spawn出一個或多個Compositor Tile Worker Thread,而後多線程並行執行SkPicture records中的繪畫操做,以以前介紹的Graphics Layer爲單位,繪製Graphics Layer裏的Render Object。同時這一過程是將Layer拆分爲多個小tile進行光柵化後寫入進tile對應的位圖中的。
    • 另外一種則是基於GPU的Hardware Rasterization,也是基於Compositor Tile Worker Thread,也是分tile進行,可是這個過程不是像Software Rasterization那樣在CPU裏繪製到位圖裏,而後再上傳到GPU中做爲紋理。而是藉助Skia’s OpenGL backend (Ganesh) 直接在GPU中的紋理中進行繪畫和光柵化,填充像素。也就是咱們常說的GPU Raster。

    如今基本最新版的幾大瀏覽器都是硬件Rasterization了,可是對於一些移動端基本仍是Software Rasterization較多。打開你的chrome瀏覽器輸入chrome://gpu/ 能夠看看你的chrome的GPU加速狀況。下圖是個人:

    使用Hardware Rasterization的好處在於:以往Software Rasterization的方式,受限於CPU和GPU以前的上傳帶寬,把位圖從RAM裏上傳到GPU的VRAM裏的過程是有不可忽視的性能開銷的。若Rasterization的區域較大,那麼使用Software Rasterization極可能在這裏出現卡頓。下面這個例子是Chrome32和Chrome41的對比,後者的版本實現了Hardware Rasterization。

    不過,對於圖片、canvas等狀況,我沒有查到究竟是怎麼處理的,可是我以爲絕對是有一個從CPU上傳到GPU的過程的,因此應該有一些狀況不是純Hardware Rasterization的,二者應該是結合使用的。另外就是硬件仍是軟件Rasterization主要仍是由設備決定的,在這個地方並無咱們手動優化的空間,可是這裏涉及到一些後面的內容,因此簡單介紹了一下。

  11. commit
    若是是Software Rasterization,全部tile的光柵化完成後Compositor Thread會commit通知GPU Thread,因而全部的tile的位圖都會做爲紋理都會被GPU Thread上傳到GPU裏。若是是使用GPU 的Hardware Rasterization,那麼此時紋理都已經在GPU中。接下來,GPU Thread會調用平臺對應的3D API(windows下是D3D,其餘平臺都是GL),把全部紋理繪製到最終的一個位圖裏,從而完成紋理的合併。
    同時,很是關鍵的一點:在紋理的合併時,藉助於3D API的相關合成參數,能夠在合併前對紋理transformations(也就是以前提到的位移、旋轉、縮放、alpha通道改變等等操做),先變形再合併。合併完成以後就能夠將內容呈現到屏幕上了。

並非每次渲染都會執行上述11步的全部步驟,好比Layout、Paint、Rasterize、commit可能一次都沒有,可是Layout又可能會不止一次。另外還有利用合成層提高來得到GPU加速的動畫等相關技術的原理。接下里就是對上述步驟更加詳細的分析。

重排 Layout、強制重排 Force Layout

重排和強制重排是老生常談的東西了,你們也應該很是熟悉了,但在這裏能夠結合瀏覽器機制順帶講一遍。

首先,若是你改了一個影響元素佈局信息的CSS樣式,好比width、height、left、top等(transform除外),那麼瀏覽器會將當前的Layout標記爲dirty,這會使得瀏覽器在下一幀執行上述11個步驟的時候執行Layout。由於元素的位置信息變了,將可能會致使整個網頁其餘元素的位置狀況都發生改變,因此須要執行Layout全局從新計算每一個元素的位置。

須要注意到,瀏覽器是在下一幀、下一次渲染的時候才重排。並非JS執行完這一行改變樣式的語句以後當即重排,因此你能夠在JS語句裏寫100行改CSS的語句,可是隻會在下一幀的時候重排一次。

若是你在當前Layout被標記爲dirty的狀況下,訪問了offsetTop、scrollHeight等屬性,那麼,瀏覽器會當即從新Layout,計算出此時元素正確的位置信息,以保證你在JS裏獲取到的offsetTop、scrollHeight等是正確的。

會觸發重排的屬性和方法:

這一過程被稱爲強制重排 Force Layout,這一過程強制瀏覽器將原本在上述渲染流程中才執行的Layout過程前提至JS執行過程當中。前提不是問題,問題在於每次你在Layout爲dirty時訪問會觸發重排的屬性,都會Force Layout,這極大的延緩了JS的執行效率。

//Layout未dirty 訪問domA.offsetWidth不會Force Layout
domA.style.width = (domA.offsetWidth + 1) + 'px' 
//Layout已經dirty, Force Layout
domB.style.width = (domB.offsetWidth + 1) + 'px' 
//Layout已經dirty, Force Layout
domC.style.width = (domC.offsetWidth + 1) + 'px'
複製代碼

這三行代碼的後兩行都致使了Force Layout,Layout一次的時間視DOM數量級從幾十微秒到十幾毫秒不等,相比於一行JS 1微秒不到的執行時間,這個開銷是難以接受的。因此也就有了讀寫分離、純用變量存儲等避免Force Layout的方法。不然你就會在你Timeline裏看到這種10屢次Recalculate Style 和 Layout的畫面了。

另外,每次重排或者強制重排後,當前Layout就再也不dirty。因此你再訪問offsetWidth之類的屬性,並不會再觸發重排。

// Layout未dirty 訪問多少次都不會觸發重排
console.log(domA.offsetWidth) 
console.log(domB.offsetWidth) 

//Layout未dirty 訪問domA.offsetWidth不會Force Layout
domA.style.width = (domA.offsetWidth + 1) + 'px' 
//Layout已經dirty, Force Layout
console.log(domC.offsetWidth) 

//Layout再也不dirty,不會觸發重排
console.log(domA.offsetWidth) 
//Layout再也不dirty,不會觸發重排
console.log(domB.offsetWidth)
複製代碼

重繪 Paint

重繪也是類似的,一旦你更改了某個元素的會觸發重繪的樣式,那麼瀏覽器就會在下一幀的渲染步驟中進行重繪。也即一些介紹重繪機制中說的invalidating(做廢),JS更改樣式致使某一片區域的樣式做廢,從而在一下幀中重繪invalidating的區域。

可是,有一個很是關鍵的行爲,就是:重繪是以合成層爲單位的。也即 invalidating的既不是整個文檔,也不是單個元素,而是這個元素所在的合成層。固然,這也是將渲染過程拆分爲Paint和Compositing的初衷之一:

Since painting of the layers is decoupled from compositing, invalidating one of these layers only results in repainting the contents of that layer alone and recompositing.

這裏給出兩個demo: demo1demo2

兩個demo幾乎徹底同樣,除了第二demo的.ab-right的樣式裏多了一行,will-change:transform;咱們在前文介紹合成層的時候強調過will-change: transform會讓元素強制提高爲合成層。

.ab-right {
        will-change: transform; //多了這行
        position: absolute;
        right: 0;
}
複製代碼

因而在第二個demo中出現了兩個合成層:HTML根元素的合成層和.ab-right所在的合成層。

而後咱們在js中修改了#target元素的樣式,因而#target元素在的合成層(即HTML根元素的合成層)被重繪。在demo1中,.ab-right元素沒有被提高爲合成層,因而.ab-right也被重繪了。而在demo2中,.ab-right元素並無重繪。先看demo1:


明顯的看到.ab-right被重繪了。


顯然,demo2只重繪了HTML根元素的合成層的內容。

對了,你還能夠順便點到Raster一欄去看看Rasterization的具體過程。前面已經介紹過了,這裏真正完成Paint裏的操做,將內容繪製進位圖或紋理中,且是分tile進行的。

重排和重繪和Compositing

先說點題外的,怎麼查看合成層:

修改一些CSS屬性如width、float、border、position、font-size、text-align、overflow-y等等會觸發重排、重繪和合成,修改另外一些屬性如color、background-color、visibility、text-decoration等等則不會觸發重排,只會重繪和合成,具體屬性列表請自行google。

接下來不少文章裏就會說,修改opacity、transform這兩個屬性僅僅會觸發合成,不會觸發重繪和合成。因此必定要用這兩個屬性來實現動畫,沒有重繪重排,效率很高。

然而事實並非這樣。

只有一個元素在被提高爲合成層以後,上述狀況才成立。

回到咱們以前說的渲染過程的第11步:

同時,很是關鍵的一點:在紋理的合併時,藉助於3D API的相關合成參數,能夠在合併前對紋理transformations(也就是以前提到的位移、旋轉、縮放、alpha通道改變等等操做),先變形再合併。合併完成以後就能夠將內容呈現到屏幕上了。

在合成多個合成層時,確實能夠藉助3D API的相關參數,從而直接實現合成層的transform、opacity效果。因此若是你將一個元素提高爲合成層,而後用JS修改其transform或opacity 或者在 transform或opacity 上施加CSS過渡或動畫,確實會避免CPU的Paint過程,由於transform和opacity能夠直接基於GPU的合成參數來完成。

可是,這是在合成層總體有transform或opacity纔會這麼作。對於沒有提高爲合成層的元素,僅僅是他本身具備transform和opacity,他是做爲合成層的內容。而生成合成層的內容和寫進位圖或紋理是在Paint和Rasterize階段完成的,所以這個元素的transform和opacity的實現也是在Paint和Rasterize中完成的。因此仍是會重排,也就沒有啓用咱們常說的GPU加速的動畫。

好比這個demo,一個提高爲合成層的div#father和一個未提高合成層的div#child,3秒鐘後JS更改child和father的transform屬性。 接下來渲染的時候流程是怎樣的?

  1. Recalc Styles(從新計算樣式)
  2. Paint 繪製變更的合成層 即 div#father
    1. Paint 繪製父元素的背景和textNode(即」父元素 提高爲合成層」)
    2. Paint 繪製child元素 即div#child
      1. Paint 先translate,完成移動
      2. Paint 再在移動後的區域裏繪製子元素的背景和textNode(即」子元素 未提高爲合成層」)
  3. Rasterize
  4. Composite 合併合成層,在合成時藉助於3D API的相關合成參數完成合成層的位移、旋轉等變換,因此div#father的translate在這裏實現

因此咱們看到了,對於未提高合成層的元素,他的transform、opacity等是在主線程裏Paint和配合Rasterize來實現的(其餘的須要重繪的屬性更是如此),依然會觸發重繪,直接用JS改動這倆屬性並不會得到性能提高。而若是元素已提高爲合成層,那麼他的transform、opacity等樣式的實現就是直接由GPU Thread控制在GPU中Compositing來完成的,主線程的Composite步驟只是計算出合成的參數,耗時極小,速度極快,因此所以就有了儘可能使用transform和opacity來完成動畫的經驗之談。

借用這篇文章中的例子:

div {
    height: 100px;
    transition: height 1s linear;
}

div:hover {
    height: 200px;
}
複製代碼

這段transition的實現過程是這樣的:

而若是代碼變成了這樣

div {
    transform: scale(0.5);
    transition: transform 1s linear;
}

div:hover {
    transform: scale(1.0);
}
複製代碼


也就是Main Thread不用重排,不用重繪,Draw也不是他完成的,他的Composite步驟只是計算出具體的Compositing參數而已(示例中其實右邊應該是Compositor和GPU Thread,可是做者爲了簡化概念、便於闡述,直接就沒有提GPU Thread,你們不要在此處扣細節)。

另外,第二個例子中div爲何提高爲合成層,其實就是前文介紹合成層的時候說的:

對 opacity、transform、fliter、backdropfilter 應用了 animation 或者 transition(須要是 active 的 animation 或者 transition,當 animation 或者 transition 效果未開始或結束後,提高合成層也會失效)

括號中的內容也很關鍵,元素在opacity等屬性具備動畫時,並非直接就提高爲合成層,而是動畫或者transition開始時才提高爲合成層,而且結束後提高合成層也失效。
同時,元素在提高爲合成層或者提高合成層失效時,會觸發重繪。這也是上圖一開始在動畫開始前有Layout the element first timePaint the element into the bitmap兩步的緣由:transition開始前,div並未被提高爲合成層,transition開始,div立馬提高合成層,立馬致使其原本所在的合成層重繪(由於要剔除掉提高爲合成層的div),而且div由於提高爲合成層,也立馬重繪,兩個重繪好的合成層Rasterize後上傳至GPU中。

demo在此,因此在動畫開始前看到:

在動畫結束後的那一幀則是這樣:

這個上述demo中,只有2個dom,因此Paint開銷幾乎能夠忽略,可是若是是dom數量多一些,那麼就極可能是下面這樣了。

實時上這個狀況不止是在動畫和過渡時,只要一個元素被提高爲合成層,在提高前和合成層失效時都會有這個過程,因此一方面是重繪帶來了繪製開銷,另外則是紋理上傳過程由於CPU到GPU的帶寬帶來的上傳開銷(雖然如今已經有Hardware Raster不用上傳,可是仍然有不能用Hardware Raster的狀況,並且Hardware Raster繪製進紋理的繪製過程自己也是有開銷的)。 所以處理很差就可能致使動畫開始前和開始後出現一幀卡頓/延遲。

最後,重要的一點,也是通常談到性能優化的文章中都會介紹的一點,即:

合成層提高並不是銀彈。

合成層提高一方面可能會引入紋理生成、上傳和重繪的開銷,並且合成層提高後會佔用GPU VRAM,VRAM可並不會很大。對於移動端,上述兩個問題尤甚。並且在介紹合成層時,我還介紹了合成層存在隱式提高的狀況。所以請合理使用。

本文主要介紹原理,因此怎麼去實現16ms的動畫、怎麼去提高渲染性能、怎麼去優化合成層數量和避免層爆炸等等、以及到底哪些狀況會提高合成層、觸發重繪等詳細內容仍是見文末附錄吧。

總結

正文算是比較詳細的介紹瀏覽器的渲染過程,可能須要你事先理解重繪、重排和合成,結合了一些demo,深刻了一些我以前理解錯的點。

這裏再次強調一下一些顛覆了我認知的內容:

  • 按照HTML5標準,scroll事件是每幀觸發一次的,自帶requestAnimationFrame節流效果
  • 按照Blink和Webkit引擎實現,touchmove、mousemove等UI input由Compositor線程接收,但傳入到主線程是每幀一次,也自帶requestAnimationFrame節流效果
  • 重繪是以合成層爲單位的
  • 合成層提高先後的Paint步驟

三週前就第一次發佈的文章終於在五一節的假期裏搞定。呼….

參考資料

chromium官方資料

渲染機制

實操&&性能優化

相關文章
相關標籤/搜索