瀏覽器渲染流程解析

前言

大家可能經(jīng)常會聽到 css 動畫比 js動畫性能更好這樣的論斷留潦,或者是“硬件加速”兔院,“層提升” 這樣的字眼孵稽;要了解這些內(nèi)容就需要對瀏覽器的渲染流程有個大致的了解菩鲜,本文就是我個人對這些內(nèi)容的一個總結(jié)梳理

需要注意的是:

  1. 本文僅個人學(xué)習(xí)總結(jié)梳理接校,如有錯漏,望指正
  2. 本文以谷歌瀏覽器Blink內(nèi)核為例诽凌,參考內(nèi)容鏈接大多需要科學(xué)上網(wǎng)
  3. 隨著谷歌瀏覽器的更新迭代皿淋,有些渲染流程或?qū)ο竺~可能發(fā)生變化(如, RenderObject 變成了 LayoutObject,RenderLayer 變成了 PaintLayer)妇拯,查看相關(guān)文檔時需要注意文檔的時間

渲染流程

先來看下blink的一個大致渲染流程幻馁,圖源谷歌的一份共享幻燈片 Life of a Pixel ,它比較全面的闡述了瀏覽的渲染流程,非常值得一看火邓,我們就借這張圖來梳理一遍

圖源 Life of a Pixel

圖中分為 渲染進(jìn)程(renderer process) 和 GPU進(jìn)程(GPU process) 兩部分铲咨,其中渲染進(jìn)程包含 主線程(main) 和 合成線程(impl)

我們可以借助谷歌開發(fā)工具的 performance 標(biāo)簽查看是否執(zhí)行了某些渲染流程步驟,我這里寫了一個簡單的html可以作為對比

<!DOCTYPE html>
<html lang="zh-cn">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>transform demo</title>
</head>
<style>
  #normal {
    display: grid;
    place-items: end;
    width: 150px;
    height: 150px;
    background-color: pink;
  }

  #compositor {
    margin-top: 20px;
    display: grid;
    place-items: end;
    width: 150px;
    height: 150px;
    background-color: palegoldenrod;
  }

  #stacking {
    display: grid;
    place-items: end;
    width: 150px;
    height: 150px;
    position: absolute;
    z-index: -1;
    top: 240px;
    background-color: skyblue;
  }

  .active {
    animation: transformAni 2s both;
  }

  @keyframes transformAni {
    to {
      transform: translate(200px);
    }
  }
</style>

<body>
  <div id="compositor">Compositor Layers</div>
  <div style="display: flex; margin-top: 20px;">
    <div id="cssBtn" style="background-color:  palegoldenrod; width: 200px;">add css animation</div>
  </div>

  <div id="stacking">The Stacking Context</div>
  <div style="display: flex; margin-top: 220px;">
    <div id="jsBtn" style="background-color: skyblue; width: 200px;"> add js animation</div>
  </div>

  <script>
    const cssBtn = document.getElementById('cssBtn')
    const compositor = document.getElementById('compositor')
    cssBtn.addEventListener('click', () => {
      compositor.classList.add("active");
    })

    const jsBtn = document.getElementById('jsBtn')
    const stacking = document.getElementById('stacking')
    jsBtn.addEventListener('click', () => {
      setInterval(() => {
        stacking.style.left = `${stacking.getBoundingClientRect().left + 10}px`
      }, 100);
    })

  </script>
</body>

</html>
![computed.png](https://upload-images.jianshu.io/upload_images/18214510-d275a5ddc6594ee9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

1. 構(gòu)建DOM樹

對應(yīng)頭圖中 DOM 節(jié)點蜓洪,由于瀏覽器本身無法直接理解和使用html纤勒,所以需要將html轉(zhuǎn)換為瀏覽器能夠理解的DOM樹,也正是因此我們才能通過js控制dom節(jié)點

圖源 Life of a Pixel

2. 樣式計算

對應(yīng)頭圖中 style 節(jié)點隆檀,不僅是html摇天,瀏覽器同樣無法直接讀懂我們寫的 css 。因此瀏覽器會將我們寫的 css 轉(zhuǎn)換成它能理解的 styleSheets 恐仑,同時計算每個 DOM 節(jié)點的樣式結(jié)果。包括將處理樣式的繼承覆蓋,將 rem 等相對單位轉(zhuǎn)換成 px,將 margin: 8 這樣的縮寫勾徽,拆開解析成 margin-left: 8若未,margin-top: 8 等具體的值隙疚∽嗜幔可以通過 computed 標(biāo)簽查看羹与。

computed.png

3. 布局計算

對應(yīng)頭圖中 layout 節(jié)點利职,這個階段也是我們很常聽到的 回流(reflow)热押,重排鬼廓。在上兩個階段結(jié)束后會生成一個儲存其計算結(jié)果的樹結(jié)構(gòu) LayoutObject Tree。在這個階段瀏覽器會遍歷 LayoutObject Tree 計算每個節(jié)點在頁面上具體的布局(比如是正常流布局党巾,或是flex布局,哪個元素該放到哪個具體的像素位置上),計算文本實際寬高等埠偿;這一階段谷歌正在重構(gòu),目前輸入和輸出都混在 LayoutObject Tree 上,之后可能會將輸出部分抽離出來

4. 分層階段

對應(yīng)頭圖中 comp.assign (compositing assignments) 節(jié)點孽拷,這個階段是我們獲取性能提升的關(guān)鍵秋茫。頁面上的元素,根據(jù)所處坐標(biāo)空間(基本可以理解為層疊上下文)不同等原因跺讯,會被劃分為不同的 PaintLayer火本,通過分層的方式保證頁面上元素以正確的順序?qū)盈B;在此基礎(chǔ)上喜最,某些特殊的PaintLayer 會被提升為合成層(Compositing Layers)秦躯,每個合成層擁有單獨的 GraphicsLayer , 而沒有被提升的 PaintLayer 則與其祖先元素共用同一個 GraphicsLayer.

它們間的對應(yīng)關(guān)系如下圖

圖源 無線性能優(yōu)化:Composite

每個 GraphicsLayer 都有一個 GraphicsContext忆谓,GraphicsContext 負(fù)責(zé)輸出該層的位圖裆装,即每層代表一份位圖踱承,GPU將位圖合成渲染到屏幕上也就是我們看到的頁面

我們可以通過開發(fā)者工具的 Layer 標(biāo)簽看到 GraphicsLayer 的分層,劃分 PaintLayer 和 提升為 GraphicsLayer 的條件具體可見 無線性能優(yōu)化:Composite (需要注意層重疊哨免,層壓縮問題)

比如我上面的例子中茎活,我給橙色的 div 加上了 will-change:transform 導(dǎo)致了層提升,而藍(lán)色的 div 與 document 共用一個 GraphicsLayer琢唾;我們還可以在 Details 標(biāo)簽看到層提升的具體原因還有內(nèi)存消耗 (tips: 層提升原因還可以看 safari 瀏覽器開發(fā)者工具的 layers 载荔,會更加具體)

layer.png

5. Pre-paint

這一階段主要有兩個任務(wù),一是判斷與上一次paint階段(見下)相比有哪些內(nèi)容需要被更新采桃,二是構(gòu)建 property trees

Paint invalidation which invalidates display items which need to be painted.

Builds paint property trees.

property treesproperty 是指 translation, scale 等需要大量計算的屬性懒熙。將這些屬性抽離出來單獨管理,避免父元素的變動導(dǎo)致其子元素上所有的屬性都有全部重新計算普办,具體見 How cc Works

6. paint

繪制階段工扎,這一階段即我們常說的重繪階段,但這一階段并不是執(zhí)行實際的頁面繪制衔蹲,而是依據(jù)頁面內(nèi)容的層疊順序生成 繪制任務(wù)列表肢娘,詳見 layer 工具,滾動滑輪可以重播繪制過程舆驶,可以觀察到橱健,同一層疊上下文情況下,先生成背景繪制任務(wù)沙廉,再生成元素內(nèi)容繪制任務(wù)拘荡,再生成更高層級的層疊上下文元素的繪制任務(wù);

主線程的任務(wù)到這里基本結(jié)束,將繪制列表提交(commit)到合成線程

7. tiling

tiling 分塊撬陵,為 GPU光柵化做準(zhǔn)備珊皿;光柵化是GPU根據(jù)繪制任務(wù)生成位圖,并將位圖儲存在內(nèi)存中袱结。大家可能聽過 CPU 光柵化的操作亮隙,這里引用一段 How cc Works 中文譯文

Chromium 目前實際支持三種不同的光柵化和合成的組合方式:軟件光柵化 + 軟件合成,軟件光柵化 + gpu 合成垢夹,gpu 光柵化 + gpu 合成溢吻。在移動平臺上,大部分設(shè)備和移動版網(wǎng)頁使用的都是 gpu 光柵化 + gpu 合成的渲染方式,理論上性能也最佳

由于這一操作需要消耗較多資源促王,為了減少資源消耗和使頁面更快呈現(xiàn)會將圖層進(jìn)行分塊( tiles )犀盟,將圖塊作為光柵化的基本單位,同時優(yōu)先對視口附近的圖塊進(jìn)行光柵化

通過rendering 標(biāo)簽蝇狼,勾選 layer borders 可以看到分塊情況阅畴,橙線是不同的 layer 而 青綠色的線則劃分了圖塊

tiling.png

8. raster

這一步由GPU執(zhí)行光柵化操作,之后的節(jié)點我沒再深入了解迅耘,大概是光柵化生成draw quads 命令贱枣,該命令會引用光柵化結(jié)果最后將內(nèi)容展現(xiàn)在屏幕上

總結(jié)

最后我們分別錄制兩個動畫的執(zhí)行流程

js 動畫

js-animation.png

可以看到 js 動畫在每次執(zhí)行時會重排重繪,執(zhí)行整個流程颤专,上面橙紅色的那條前面有寫到 Layout Shift纽哥,即 布局提升,也就是我們說的強制重排栖秕,因為我們在 js 腳本里執(zhí)行了 stacking.getBoundingClientRect().left 訪問元素位置春塌,這就需要立刻重排來計算元素當(dāng)前的位置

css動畫

css-animation.png

可以看到,css動畫主線程上沒有進(jìn)行重排重繪

梳理完整個流程簇捍,我們就能理解開頭提到的內(nèi)容了只壳,關(guān)鍵點就在于分層合成

“層提升” 即文中的 分層階段;

“硬件加速” 即 GPU加速暑塑,一些可能導(dǎo)致頁面大范圍重排重繪(如 translate動畫)吼句,或需要大量簡單計算的任務(wù)(如 filter動畫)都會導(dǎo)致層提升,將這部分任務(wù)交由GPU處理梯投,將處理完后的結(jié)果再合成到頁面上命辖;

而 css 動畫性能更優(yōu)的原因是:

  1. 避免了通過js訪問元素的位置信息導(dǎo)致強制重排
  2. css動畫元素移動時在合成層上進(jìn)行,避免了頁面重排
  3. 合成由 GPU 進(jìn)程控制分蓖,即使 js 阻塞主線程尔艇,css動畫也能正常執(zhí)行

層提升會加大內(nèi)存消耗,加大移動端設(shè)備負(fù)擔(dān)么鹤,需要酌情使用

補充

will-change

上文我們的例子提到了 will-change 屬性终娃,它的作用是提前告知瀏覽器可能變動的屬性,讓瀏覽器提前做好準(zhǔn)備蒸甜,提前進(jìn)行相關(guān)計算等棠耕,它有以下取值

  • auto 讓瀏覽器自己猜哪些值會變動
  • scroll-position 表示滾動條位置可能發(fā)生變化或產(chǎn)生動畫
  • contents 表示元素內(nèi)容可能變動或產(chǎn)生動畫
  • <custom-ident> 表示所有css屬性

基本上哪里的css屬性變化導(dǎo)致了頁面的卡頓都可以使用 will-change 優(yōu)化

我們的例子中已經(jīng)寫入了 will-change: transform ,因此瀏覽器一開始就幫我們做了層提升準(zhǔn)備柠新,所以橙色 div 一開始在頁面上就是分層的情況窍荧。而如果我們?nèi)サ暨@個屬性,觀察 layer 會發(fā)現(xiàn)橙色 div 一開始在頁面上并沒有層提升恨憎,只有在執(zhí)行動畫時才進(jìn)行了層提升蕊退,動畫結(jié)束后層提升又消失了

使用該屬性同樣要注意的是內(nèi)存消耗問題郊楣,因為瀏覽器會提前進(jìn)行優(yōu)化計算并儲存計算結(jié)果。由于瀏覽器本身已經(jīng)做了十足的性能優(yōu)化瓤荔,因此在頁面沒出現(xiàn)動畫卡頓之前沒有必要使用該屬性净蚤,如果需要使用也盡量通過以下形式:

.will-change-parent:hover .will-change {
  will-change: transform;
}
.will-change {
  transition: transform 0.3s;
}
.will-change:hover {
  transform: scale(1.5);
}

當(dāng)父元素 hover 時,給子元素加上 will-change输硝,hover 失效則移出今瀑,既給了瀏覽器準(zhǔn)備的時間,又避免了一直掛著該屬性帶來的資源消耗

requestAnimationFrame / requestIdleCallback

講到動畫我們就順便提一嘴 requestAnimationFramerequestIdleCallback

我們看到的動畫都是由屏幕快速播放一系列連貫的圖片組成点把,為了讓人眼感受不到卡頓橘荠,大多數(shù)屏幕的刷新頻率都是60Hz,即一秒鐘刷新六十次屏幕愉粤,每次刷新叫做一幀砾医,一幀時間大約16.7ms,如果一幀的渲染時間超過這個數(shù)就會導(dǎo)致動畫看起來出現(xiàn)了卡頓衣厘,一幀流程大致如下圖

圖源 The Anatomy of a Frame

requestAnimationFrame會在每一幀的渲染流程執(zhí)行前都執(zhí)行一次,因此使用js實現(xiàn)動畫時压恒,相比于 setInterval 實際執(zhí)行時間的不確定性requestAnimationFrame 更加可靠影暴;

requestIdleCallback 則是在每一幀結(jié)束前判斷是否有剩余時間,如果有則執(zhí)行探赫,無則不執(zhí)行

參考鏈接

  1. Life of a Pixel

  2. chromium renderer/core/paint

  3. 無線性能優(yōu)化:Composite

  4. How cc Works / 中文

  5. How Blink works / 中文

  6. RenderingNG deep-dive: BlinkNG / 中文

  7. The Anatomy of a Frame

  8. 《css新世界》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末型宙,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子伦吠,更是在濱河造成了極大的恐慌妆兑,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毛仪,死亡現(xiàn)場離奇詭異搁嗓,居然都是意外死亡,警方通過查閱死者的電腦和手機箱靴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門腺逛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人衡怀,你說我怎么就攤上這事棍矛。” “怎么了抛杨?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵够委,是天一觀的道長。 經(jīng)常有香客問我怖现,道長茁帽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮脐雪,結(jié)果婚禮上厌小,老公的妹妹穿的比我還像新娘。我一直安慰自己战秋,他們只是感情好璧亚,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著脂信,像睡著了一般癣蟋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上狰闪,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天疯搅,我揣著相機與錄音,去河邊找鬼埋泵。 笑死幔欧,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的丽声。 我是一名探鬼主播礁蔗,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼雁社!你這毒婦竟也來了浴井?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤霉撵,失蹤者是張志新(化名)和其女友劉穎磺浙,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體徒坡,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡撕氧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了崭参。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片呵曹。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖何暮,靈堂內(nèi)的尸體忽然破棺而出奄喂,到底是詐尸還是另有隱情,我是刑警寧澤海洼,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布跨新,位于F島的核電站,受9級特大地震影響坏逢,放射性物質(zhì)發(fā)生泄漏域帐。R本人自食惡果不足惜赘被,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望肖揣。 院中可真熱鬧民假,春花似錦、人聲如沸龙优。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽彤断。三九已至野舶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間宰衙,已是汗流浹背平道。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留供炼,地道東北人一屋。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像劲蜻,于是被迫代替她去往敵國和親陆淀。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容