探尋瀏覽器渲染的秘密
- 2019 年 10 月 4 日
- 筆記
文章由蘑菇街同事「藍鯨」投稿。
前言
起因是這樣,有運營小姐姐跟我回饋某個頁面卡頓的厲害。心中突然一想,媽耶不會有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