探寻浏览器渲染的秘密
(給前端大全加星標(biāo),提升前端技能)
作者:前端桃園 公號 / 桃翁
前言
起因是這樣,有運營小姐姐跟我反饋某個頁面卡頓的厲害。心中突然一想,媽耶不會有bug吧,心慌慌的。然后自己打開頁面,不卡呀,流暢的一xx,肯定是她弄錯了。帶著去教她如何正確的使用電腦的想法我自信的下了樓,然后自信的在她電腦上打開了頁面,我滑,我滑,我再滑。woc,頁面咋不動啊,woc,電腦都卡死了。???什么情況,然后有其他運營反饋 air 上并不卡頓。頁面下滑為何卡頓?在mbp和mba上的表現(xiàn)為何不同?這一切的問題究竟是從何而起?請老板們帶著這兩個問題往下看,我將一步一步揭開瀏覽器渲染的面紗。
先上張圖讓大家感受一下被支配的恐懼。注意,那個 GPU 進程內(nèi)存空間占用 10.9 GB。
mbp上知識儲備
要搞懂我下面說的,首先你需要先知道現(xiàn)代瀏覽器的架構(gòu)以及顯卡、GPU 和屏幕分辨率的關(guān)系。當(dāng)然了,就算這些不了解,也是可以接著往下看的,我會簡單的講一下,嘻嘻嘻。
現(xiàn)代瀏覽器的架構(gòu)
因為這里并沒有什么規(guī)范,各大瀏覽器廠商的各自的架構(gòu)設(shè)計也并不相同(不過都是大同小異),我就以 chrome 瀏覽器為例說一下 chrome 的設(shè)計。
chrome 瀏覽器從最初的單進程發(fā)展到現(xiàn)在的多進程架構(gòu)。我們可以從上面我發(fā)的圖看到瀏覽器包括:一個瀏覽器進程、一個 GPU 進程、一個網(wǎng)絡(luò)進程、多個渲染進程和多個插件進程。
渲染進程
了解了上面的瀏覽器的架構(gòu),下面我們說說今天的主角渲染進程,關(guān)于瀏覽器多進程之間是如何配合最后在屏幕上展示內(nèi)容的,這個后面會寫文章記錄?,F(xiàn)在我們說說渲染進程的事兒。
渲染流水線按照渲染的時間順序可以分成以下幾個子階段:構(gòu)建 DOM 樹、樣式計算、布局、分層、繪制、分塊、光柵化、合成。東西有點多,為了快速記憶和理解,需要重點關(guān)注每個子階段的輸入和輸出以及做了哪些處理。
還需要了解渲染進程中的幾個線程。包括主線程(main thread)、工作線程(work thread)、合成線程(compositor thread)以及光柵化線程(raster thread)。后面會總結(jié)這些線程的具體功能,我們先看一下整體的渲染流程。
構(gòu)建 DOM 樹
構(gòu)建 DOM 樹DOM 樹是什么相信大家都知道,我就不多 BB 了。因為瀏覽器無法直接理解和使用 html 文件,所以需要將 html 文件轉(zhuǎn)為瀏覽器能夠理解的結(jié)構(gòu) DOM 樹。
網(wǎng)頁中常常包含圖片、css、js 等資源文件,這些資源瀏覽器會去各種渠道獲取(緩存、網(wǎng)絡(luò)下載等)。在構(gòu)建 DOM 樹的時候主線程會去請求他們,相關(guān)資源會通過進程之間的通信(IPC)通知網(wǎng)絡(luò)進程去下載這些資源。在遇到?<script>?標(biāo)簽的時候,解析 DOM 樹的工作會暫停,等 js 代碼執(zhí)行完畢之后在去重新解析 DOM 樹。
總結(jié)一下構(gòu)建 DOM 樹子階段的輸入、輸出以及操作過程:
輸入:html 文件
輸出:DOM 樹
操作過程:解析 html 結(jié)構(gòu)為瀏覽器可以理解的 DOM 樹結(jié)構(gòu),期間會去下載次級資源以及執(zhí)行 js 代碼。
樣式計算
樣式計算是為了獲取每個節(jié)點的樣式,其主要分為三步來完成。
樣式計算首先和解析 DOM 樹一樣,瀏覽器是無法理解 css 代碼的,需要將 css 文件轉(zhuǎn)成瀏覽器可以理解的數(shù)據(jù)結(jié)構(gòu)?styleSheets。具體 styleSheets 是什么樣的結(jié)構(gòu)這里我們就不去重點了解了,只需要了解到主進程會將 css 代碼轉(zhuǎn)成瀏覽器可以理解的結(jié)構(gòu),這個結(jié)構(gòu)支持查詢和修改??梢栽陂_發(fā)者工具上通過 document.styleSheets 打印出來。
為了適配多端樣式,我們可能使用的是 rem、vh 等 css 代碼。這些屬性值不容被渲染引擎理解,所以需要將這些不是標(biāo)準(zhǔn)化的樣式轉(zhuǎn)為標(biāo)準(zhǔn)樣式。比如 rem 轉(zhuǎn)成 px、bule 轉(zhuǎn)成 rgba 等。
我們獲取到標(biāo)準(zhǔn)化后的樣式表,最后就是計算每個節(jié)點的樣式了。這一步驟涉及到 css 的繼承規(guī)則和層疊規(guī)則。有些屬性是可以被子元素繼承的,有些屬性是會覆蓋前面的樣式。這一塊也不多做討論了。
總結(jié)一下樣式計算子階段的輸入、輸出和操作過程:
輸入:css 樣式文件
輸出:對應(yīng)每個 DOM 的樣式
操作過程:進行了三個操作,包括:轉(zhuǎn)成瀏覽器可以理解的 styleSheets、將 css 轉(zhuǎn)成標(biāo)準(zhǔn)化的樣式、最后是計算每個節(jié)點的樣式。
布局階段
想要渲染一個完整的頁面,僅知道 DOM 樹和 DOM 樹元素的樣式還是不夠的,我們還需要知道 DOM 樹中元素的位置。
布局階段同樣的布局這個子階段也分為兩個過程操作,分別是合成布局樹和計算節(jié)點位置。
布局樹和 DOM 樹類似,不過布局樹上只包含會顯示的節(jié)點內(nèi)容,不包含如 ?等元素。也不包含 display: none 樣式的元素。只包含可見節(jié)點。有了一顆完成的布局樹,主線程會計算出每個元素的位置信息以及盒子大小。
總結(jié)一下布局階段子階段的輸入、輸出和操作過程:
輸入:css 樣式表、DOM 樹
輸出:布局樹
操作過程:合成布局樹、計算節(jié)點位置
分層
有了布局樹,計算出了每個節(jié)點的位置。那么下面是不是進行繪制了呢?答案是否定的,因為頁面有很多復(fù)雜的效果,比如滑動、z-idnex 等。為了更好的實現(xiàn)這些效果,渲染引擎主線程還需要為特定的階段生成專用的圖層,并生成一顆對應(yīng)的圖層樹。
分層分層這一步其實沒什么好解釋了,唯一需要了解的是哪些元素會被單獨分層。布局樹和圖層樹并不是一一對應(yīng)的關(guān)系,不是每個布局樹的節(jié)點都會生成一個單獨的圖層樹節(jié)點。如果一個節(jié)點沒有對應(yīng)的層,那么這個節(jié)點就從屬于父節(jié)點的圖層。那么哪些操作會讓節(jié)點生成一個單獨的圖層呢?接著往下面看。
1)擁有層疊上下文屬性的元素會單獨生成一個圖層。
瀏覽器是一個二維的概念,但是層疊上下文可以讓元素具有三維的概念。比如 css 屬性中的 z-index、position、css 濾鏡等。
3D 或透視變換的 css 屬性
使用加速視頻解碼的 video 元素
canvas 元素
opacity 屬性
2)需要裁剪的地方也會單獨生成一個圖層
裁剪就是需要滾動的地方,里面內(nèi)容會單獨生成一個圖層。如果有滾動條,滾動條也會單獨生成一個圖層。(所以想一想我那個性能很差的頁面有多少個圖層?手動狗頭)
總結(jié)一下布局階段子階段的輸入、輸出和操作過程:
輸入:布局樹
輸出:圖層樹
操作過程:為特定的節(jié)點生成單獨的圖層、并將這些圖層合成圖層樹
圖層繪制
在完成圖層樹的構(gòu)建之后,渲染引擎主線程會對每個圖層進行繪制。這里說的繪制不是真正的繪制畫面,而是生成一個繪制指令列表。
圖層繪制如果我們要在白紙上繪制一些東西,比如黃底、白圓、黑字的一個圖案。通常我們會把操作分解成幾步來完成:
我們會先在白紙上涂上黃色的底。
然后我們會在黃底上畫一個白色的圓。
最后我們會在白色圓上畫出黑色的字。
渲染引擎的圖層繪制和這個類似,會把每一個圖層的繪制拆分成很多的繪制指令。
總結(jié)一下布局階段子階段的輸入、輸出和操作過程:
輸入:圖層樹
輸出:每個圖層的繪制指令
操作過程:將每個圖層的繪制拆分成多個繪制指令,傳給合成線程。
柵格化
繪制列表只是用來生成記錄繪制指令的列表,實際的繪制操作是有渲染進程的合成線程來執(zhí)行的。
柵格化繪制指令生成之后,渲染進程主線程會將繪制指令發(fā)送給合成線程,由合成線程來完成最后的繪制工作。合成線程會將圖層劃分為圖塊。簡單解釋下圖塊是什么,瀏覽器的視口內(nèi)容是有限的,有些圖層可能非常大。渲染進程不會把該圖層的所有內(nèi)容都渲染出來,而是會將這些圖層劃分為一個一個小的圖塊。柵格化子進程會將視口區(qū)域內(nèi)的圖塊轉(zhuǎn)化為位圖(磁貼),并將這位存入 GPU 顯存中。GPU 操作是在 GPU 進程中,所以渲染進程會通過 IPC 通信協(xié)議來通知 GPU 進程來進行操作。
總結(jié)一下布局階段子階段的輸入、輸出和操作過程:
輸入:繪制指令列表、圖層樹。
輸出:位圖
操作過程:將圖層劃分為圖塊,將圖塊轉(zhuǎn)換成位圖。
合成和顯示
等所有圖塊都被柵格化,合成線程會收集位圖信息來創(chuàng)建合成幀。合成幀隨后會通過 IPC 協(xié)議將消息傳給瀏覽器主進程。瀏覽器主進程收到消息后,會將頁面內(nèi)容繪制到內(nèi)存中,最后再將內(nèi)存顯示在屏幕上。
總結(jié)
到這里,我們整個瀏覽器的渲染進程也就講完了。下面我們通過一張圖來總結(jié)一下渲染過程中,瀏覽器各進程各線程是如何工作的。
總結(jié)主線程將 html 文件轉(zhuǎn)化為瀏覽器能夠讀懂的?DOM 樹結(jié)構(gòu)。其中會通過網(wǎng)絡(luò)進程加載次級資源,遇到 js 會停止構(gòu)建 DOM 樹,并執(zhí)行 js。
主線程將 css 文件轉(zhuǎn)化為瀏覽器能夠讀懂的?styleSheets?結(jié)構(gòu),并將其中的屬性標(biāo)準(zhǔn)化,最后計算每個節(jié)點的樣式。
主線程通過得到的 DOM 樹和 styleSheets 樣式表合成一顆布局樹并計算每個節(jié)點的具體位置。
主線程通過得到的布局樹進行圖層分層并得到一個圖層樹。
主線程通過分層樹對每一個圖層分解繪制指令,得到一個繪制指令列表。
合成線程對圖層進行分塊處理,并對視口區(qū)域內(nèi)的圖塊進行位圖轉(zhuǎn)換,將得到的結(jié)果通過?GPU 進程存入到?GPU 顯存中。
合成線程收集位圖信息創(chuàng)建合成幀,并將消息通過 IPC 協(xié)議傳給瀏覽器主進程,主進程收到消息后,會將頁面內(nèi)容繪制到內(nèi)存中,最后再將內(nèi)存顯示在屏幕上。
上面已經(jīng)講完了瀏覽器整個渲染流程,我們來講講產(chǎn)生這個例子中產(chǎn)生卡頓的原因。通常情況下圖層是有助于性能的,但是創(chuàng)建的每一層都需要內(nèi)存和管理,而這些并不是免費的。事實上,在內(nèi)存有限的設(shè)備上,對性能的影響可能遠(yuǎn)遠(yuǎn)超過創(chuàng)建層帶來的任何好處。每一層的紋理都需要上傳到 GPU,使 CPU 與 GPU 之間的帶寬、GPU 上可用于紋理處理的內(nèi)存都受到進一步限制。
屏幕分辨率、顯卡等關(guān)系
講完了渲染流程,也找到了頁面卡頓的原因。但是我們還是不知道為何頁面在 mbp 和 mba 上有差異。這就是接下來我們要講的內(nèi)容了。
我們需要了解幾個概念:屏幕尺寸、分辨率、屏幕像素密度。
屏幕尺寸,單位通常是英寸,其大小是顯示器的對角線長度。
分辨率也就是屏幕上由多少個像素組成,mbp 的屏幕分辨率是 2560 * 1600,也就是在橫向的寬度上有 2560 個像素,豎向的高度上有 1600 個像素。
屏幕像素密度(ppi ),每英寸屏幕有多少個像素。
mbp 的屏幕分辨率是 2560 * 1600,mba 的屏幕分辨率是 1440 * 900。這樣算下來 mbp 有 409600 個像素,mba 有1296000 個像素。顯卡壓力會小很多,內(nèi)存占用也會更少。再有因為整個布局是 table 布局,每次滑動都會導(dǎo)致整個 table 表格回流,導(dǎo)致整個 GPU 內(nèi)存飆升。
總結(jié)
至此整個問題就全部解決、全部了解清楚了。其實剛開始就把這個問題解決了,但是其中很多東西一直都不怎么了解,趁著這次機會把整個過程都了解清楚。其實像我們這種做開發(fā)的人,就是要有一種死鉆牛角的精神,不能把問題解決了就行了,更要了解其中的原理,為什么會這樣。期間我也有想放棄不整了,還是在小伙伴的幫助下完成這次的探尋之旅。在畢業(yè)初期能夠遇到一個和自己講的來話的學(xué)長真的能給自己很大的幫助。
共勉。
最后放一張解決了問題后的圖。
解決后參考鏈接
極客時間《瀏覽器工作原理與實踐》第5、6講
https://zhuanlan.zhihu.com/p/47407398
https://developers.google.com/web/fundamentals/performance/rendering/stick-to-compositor-only-properties-and-manage-layer-count
https://my.oschina.net/u/2282680/blog/805130
https://www.zhihu.com/question/268016229
https://www.jianshu.com/p/c3387bcc4f6e
推薦閱讀
(點擊標(biāo)題可跳轉(zhuǎn)閱讀)
燒腦!JS+Canvas 帶你體驗「偶消奇不消」的智商挑戰(zhàn)
Google 員工吐槽 TypeScript :類型檢查不太好
如何讓你的 JS 寫得更漂亮
覺得本文對你有幫助?請分享給更多人
關(guān)注「前端大全」加星標(biāo),提升前端技能
好文章,我在看??
總結(jié)
以上是生活随笔為你收集整理的探寻浏览器渲染的秘密的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 干货 | Elasticsearch7.
- 下一篇: Memcached----2-3