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