Chrome 頁面呈現原理與性能優化之企業級分享總結

背景

前段時間梳理了一下瀏覽器相關的知識,還做了一個公司級的分享,60多人過來聽了我的分享,感覺還行,哈哈。先看一下分享目錄:

本篇文章,如果直接貼ppt圖,理解起來可能比較費勁,這裏就大概講一下內容,再附上我之前已經把部分內容輸出了完整的文章的鏈接,方便大家結合ppt來理解,因此本文結合ppt食用效果更佳哦~


Chrome 基本架構介紹

整體架構

瀏覽器的主要功能就是向服務器發出請求,在瀏覽器窗口中展示您選擇的網絡資源,這裏所說的資源一般是指 HTML 文檔,也可以是 PDF、圖片或其他的類型。大體上,瀏覽器可以分爲五部分:

  • 用戶界面,主要負責展示頁面中,除了 page 本身的內容,我們可以粗略地理解爲打開一個空頁面的時候呈現的界面就是瀏覽器的用戶界面(GUI)。

  • 瀏覽器引擎,這裏個人認爲主要指的是在用戶界面和渲染引擎之間傳遞指令,以及調度瀏覽器各方面的資源,協調爲呈現頁面、完成用戶指令而工作。

  • 呈現引擎,按圖中看,包含了一個 compositor(合成器)和 Javascript Engine(JS解釋引擎)。分別是負責解析 HTML 和 CSS 內容,並將解析後的內容顯示在屏幕上 和 用於解析和執行 JavaScript 代碼。

  • 後端服務層,這裏包含了一些後端服務。比如網絡請求層(network)、數據存儲,瀏覽器需要在硬盤上保存各種數據,例如 Cookie、Storage等。

  • 特別服務層,這裏主要指的是一些瀏覽器自帶的服務,比如你填完某個網站的賬號密碼,瀏覽器可以幫你記住賬號密碼,又比如開啓瀏覽器的暗黑模式等特殊的服務。

以上,對前端來說,比較重要的是渲染引擎(一些文章也叫瀏覽器引擎)。我們可以看看都有哪些渲染引擎的內核。

多進程架構

早期的web瀏覽器是單進程的,發生⻚⾯⾏爲不當、瀏覽器錯誤、瀏覽器插件等錯誤都會引起整個瀏覽器或當前運 ⾏的選項卡關閉。因此Chrome將chromium應⽤程序放在相互隔離的獨⽴的進程,也就是多進程的一個架構。

多進程的優勢有:

  • 防⼀個⻚⾯崩潰影響整個瀏覽器

  • 安全性和沙盒,由於操作系統提供了限制進程權限的方法,因此瀏覽器可以從某些功能中,對某些進程進行沙箱處理。例如,Chrome 瀏覽器可以對處理用戶輸入(如渲染器)的進程,限制其文件訪問的權限。

  • 進程有⾃⼰的私有內存空間,可以擁有更多的內存。

多進程的劣勢有:

  • 給每個進程分配了單獨的內存,儘管Chrome本身有一些優化策略,比如爲了節省內存,Chrome限制了它可以啓動的進程數量。限制因設備的內存和CPU功率⽽異,但當Chrome達到限制時,它會在⼀個進程中開始從同⼀站點運⾏多個選項卡。

  • 有更高的資源佔用。因爲每個進程都會包含公共基礎結構的副本(如 JavaScript 運行環境),這就意味着瀏覽器會消耗更多的內存資源。

多進程的架構,還有優化的地方,因此 Chrome 未來的架構是一個面向服務的架構,將瀏覽器程序的每個部分,作爲一項服務運行,從而可以輕鬆拆分爲不同的流程或彙總爲同一個流程。這樣可以做到,當 Chrome 在強大的硬件上運行時,它可能會將每個服務拆分爲不同的進程,從而提供更高的穩定性,但如果它位於資源約束的設備上,Chrome 會將服務整合到一個進程中,從而整合流程以減少內存使用。

關於架構這章,更詳細的內容可以看我這篇文章,《一文帶你看透 Chrome 瀏覽器架構


瀏覽器中頁面渲染過程

按照渲染的時間順序,流水線可分爲如下幾個子階段:構建 DOM 樹、樣式計算、佈局階段、分層、柵格化和顯示。如圖:

  1. 渲染進程將 HTML 內容轉換爲能夠讀懂DOM 樹結構。

  2. 渲染引擎將 CSS 樣式錶轉化爲瀏覽器可以理解的styleSheets,計算出 DOM 節點的樣式。

  3. 創建佈局樹,並計算元素的佈局信息。

  4. 對佈局樹進行分層,並生成分層樹。

  5. 爲每個圖層生成繪製列表,並將其提交到合成線程。合成線程將圖層分圖塊,並柵格化將圖塊轉換成位圖。

  6. 合成線程發送繪製圖塊命令給瀏覽器進程。瀏覽器進程根據指令生成頁面,並顯示到顯示器上。

構建 DOM 樹

瀏覽器從網絡或硬盤中獲得HTML字節數據後會經過一個流程將字節解析爲DOM樹,先將HTML的原始字節數據轉換爲文件指定編碼的字符,然後瀏覽器會根據HTML規範來將字符串轉換成各種令牌標籤,如html、body等。最終解析成一個樹狀的對象模型,就是dom樹。

具體步驟:

  1. 轉碼(Bytes -> Characters)—— 讀取接收到的 HTML 二進制數據,按指定編碼格式將字節轉換爲 HTML 字符串

  2. Tokens 化(Characters -> Tokens)—— 解析 HTML,將 HTML 字符串轉換爲結構清晰的 Tokens,每個 Token 都有特殊的含義同時有自己的一套規則

  3. 構建 Nodes(Tokens -> Nodes)—— 每個 Node 都添加特定的屬性(或屬性訪問器),通過指針能夠確定 Node 的父、子、兄弟關係和所屬 treeScope(例如:iframe 的 treeScope 與外層頁面的 treeScope 不同)

  4. 構建 DOM 樹(Nodes -> DOM Tree)—— 最重要的工作是建立起每個結點的父子兄弟關係

樣式計算

渲染引擎將 CSS 樣式錶轉化爲瀏覽器可以理解的 styleSheets,計算出 DOM 節點的樣式。

CSS 樣式來源主要有 3 種,分別是通過 link 引用的外部 CSS 文件、style標籤內的 CSS、元素的 style 屬性內嵌的 CSS。,其樣式計算過程主要爲:

可以看到上面的 CSS 文本中有很多屬性值,如 2em、blue、bold,這些類型數值不容易被渲染引擎理解,所以需要將所有值轉換爲渲染引擎容易理解的、標準化的計算值,這個過程就是屬性值標準化。處理完成後再處理樣式的繼承和層疊,有些文章將這個過程稱爲CSSOM的構建過程。

頁面佈局

佈局過程,即排除 script、meta 等功能化、非視覺節點,排除 display: none 的節點,計算元素的位置信息,確定元素的位置,構建一棵只包含可見元素佈局樹。如圖:

其中,這個過程需要注意的是迴流和重繪,關於迴流和重繪,詳細的可以看我另一篇文章《瀏覽器相關原理(面試題)詳細總結二》,這裏就不說了~

生成分層樹

頁面中有很多複雜的效果,如一些複雜的 3D 變換、頁面滾動,或者使用 z-indexing 做 z 軸排序等,爲了更加方便地實現這些效果,渲染引擎還需要爲特定的節點生成專用的圖層,並生成一棵對應的圖層樹(LayerTree),如圖:

如果你熟悉 PS,相信你會很容易理解圖層的概念,正是這些圖層疊加在一起構成了最終的頁面圖像。在瀏覽器中,你可以打開 Chrome 的"開發者工具",選擇"Layers"標籤。渲染引擎給頁面分了很多圖層,這些圖層按照一定順序疊加在一起,就形成了最終的頁面。

並不是佈局樹的每個節點都包含一個圖層,如果一個節點沒有對應的層,那麼這個節點就從屬於父節點的圖層。那麼需要滿足什麼條件,渲染引擎纔會爲特定的節點創建新的層呢?詳細的可以看我另一篇文章《瀏覽器相關原理(面試題)詳細總結二》,這裏就不說了~

柵格化

合成線程會按照視口附近的圖塊來優先生成位圖,實際生成位圖的操作是由柵格化來執行的。所謂柵格化,是指將圖塊轉換爲位圖。如圖:

 

通常一個頁面可能很大,但是用戶只能看到其中的一部分,我們把用戶可以看到的這個部分叫做視口(viewport)。在有些情況下,有的圖層可以很大,比如有的頁面你使用滾動條要滾動好久才能滾動到底部,但是通過視口,用戶只能看到頁面的很小一部分,所以在這種情況下,要繪製出所有圖層內容的話,就會產生太大的開銷,而且也沒有必要。

顯示

最後,合成線程發送繪製圖塊命令給瀏覽器進程。瀏覽器進程根據指令生成頁面,並顯示到顯示器上,渲染過程完成。


瀏覽器中的JavaScript運行機制

JavaScript如何工作的,首先要理解幾個概念,分別是JS Engine(JS引擎)、Context(執行上下文)、Call Stack(調用棧)、Event Loop(事件循環)。

JS Engine(JS引擎)

JavaScript引擎就是用來執行JS代碼的, 通過編譯器將代碼編譯成可執行的機器碼讓計算機去執行。目前比較流行的就是V8引擎,Chrome瀏覽器和Node.js採用的引擎就是V8引擎。引擎主要由堆(Memory Heap)和棧(Call Stack)組成。

  • Heap(堆) - JS引擎中給對象分配的內存空間是放在堆中的

  • Stack(棧)- 這裏存儲着JavaScript正在執行的任務。每個任務被稱爲幀(stack of frames)

Context(執行上下文)

執行上下文是 JavaScript 執行一段代碼時的運行環境,比如調用一個函數,就會進入這個函數的執行上下文,確定該函數在執行期間用到的諸如 this、變量、對象以及函數等。

JavaScript 中有三種執行上下文類型。

  1. 全局執行上下文 — 這是默認或者說基礎的上下文,任何不在函數內部的代碼都在全局上下文中。它會執行兩件事:創建一個全局的 window 對象(瀏覽器的情況下),並且設置 this 的值等於這個全局對象。一個程序中只會有一個全局執行上下文。

  2. 函數執行上下文 — 每當一個函數被調用時, 都會爲該函數創建一個新的上下文。每個函數都有它自己的執行上下文,不過是在函數被調用時創建的。函數上下文可以有任意多個。每當一個新的執行上下文被創建,它會按定義的順序(將在後文討論)執行一系列步驟。

  3. Eval 函數執行上下文 — 執行在 eval 函數內部的代碼也會有它屬於自己的執行上下文,但由於 JavaScript 開發者並不經常使用 eval,所以在這裏我不會討論它。

創建執行上下文有兩個階段:1) 編輯(創建)階段 和 2) 執行階段。舉個例子:

Call Stack(調用棧)

JavaScript 引擎正是利用棧的這種結構來管理執行上下文的。在執行上下文創建好後,JavaScript 引擎會將執行上下文壓入棧中,通常把這種用來管理執行上下文的棧稱爲執行上下文棧,又稱調用棧。

瀏覽器中查看調用棧的方法:

  • 當你執行一段複雜的代碼時,你可能很難從代碼文件中分析其調用關係,這時候你可以在你想要查看的函數中加入斷點,然後當執行到該函數時,就可以查看該函數的調用棧了。

  • console.trace()

調用棧是有大小的,當入棧的執行上下文超過一定數目,JavaScript 引擎就會報錯,我們把這種錯誤叫做棧溢出。正常業務需求一般不會發生棧溢出的錯誤,只有遞歸忘記寫邊界的時候會出現棧溢出,我們寫代碼的時候要注意一下。

Event Loop(事件循環)

JavaScript代碼的執行過程中,除了依靠函數調用棧來搞定函數的執行順序外,還依靠任務隊列(task queue)來搞定另外一些代碼的執行。整個執行過程,我們成爲事件循環過程。一個線程中,事件循環是唯一的,但是任務隊列可以擁有多個。任務隊列又分爲macro-task(宏任務)與micro-task(微任務),在最新標準中,它們被分別稱爲task與jobs。

macro-task大概包括:

  • script(整體代碼)

  • setTimeout

  • setInterval

  • setImmediate

  • I/O

  • UI rendering

micro-task大概包括:

  • process.nextTick

  • Promise

  • Async/Await(實際就是promise)

  • MutationObserver(html5新特性)

整體執行,我畫了一個流程圖:

 

總的結論就是,執行宏任務,然後執行該宏任務產生的微任務,若微任務在執行過程中產生了新的微任務,則繼續執行微任務,微任務執行完畢後,再回到宏任務中進行下一輪循環。舉個例子:

結合流程圖理解,答案輸出爲:async2 end => Promise => async1 end => promise1 => promise2 => setTimeout


垃圾回收與內存泄露

通常情況下,垃圾數據回收分爲手動回收和自動回收兩種策略。

  • 手動回收策略,何時分配內存、何時銷燬內存都是由代碼控制的。

  • 自動回收策略,產生的垃圾數據是由垃圾回收器來釋放的,並不需要手動通過代碼來釋放。

V8 中會把堆分爲新生代和老生代兩個區域,新生代中存放的是生存時間短的對象,老生代中存放的生存時間久的對象。

新生代算法

新生代中用Scavenge 算法來處理,把新生代空間對半劃分爲兩個區域,一半是對象區域,一半是空閒區域。新加入的對象都會存放到對象區域,當對象區域快被寫滿時,就需要執行一次垃圾清理操作。

在新生代空間中,內存空間分爲兩部分,分別爲 From 空間和 To 空間。在這兩個空間中,必定有一個空間是使用的,另一個空間是空閒的。新分配的對象會被放入 From 空間中,當 From 空間被佔滿時,新生代 GC 就會啓動了。算法會檢查 From 空間中存活的對象並複製到 To 空間中,如果有失活的對象就會銷燬。當複製完成後將 From 空間和 To 空間互換,這樣 GC 就結束了。

爲了執行效率,一般新生區的空間會被設置得比較小,也正是因爲新生區的空間不大,所以很容易被存活的對象裝滿整個區域。爲了解決這個問題,JavaScript 引擎採用了對象晉升策略,也就是經過兩次垃圾回收依然還存活的對象,會被移動到老生區中。

老生代算法

老生代中用 標記 - 清除(Mark-Sweep)和 標記 - 整理(Mark-Compact)的算法來處理。標記階段就是從一組根元素開始,遞歸遍歷這組根元素(遍歷調用棧),能到達的元素稱爲活動對象,沒有到達的元素就可以判斷爲垃圾數據.然後在遍歷過程中標記,標記完成後就進行清除過程。

算法比較

在上述三種算法執行時,都需要將暫停應用邏輯(JS 執行),GC 完成後再執行應用邏輯。此時會有一個停頓時間(稱爲全停頓,stop-the-world)故 V8 採用了增量標記(Incremental Marking)算法,將標記過程分爲一個個的子標記過程,同時讓垃圾回收標記和 JavaScript 應用邏輯交替進行,直到標記階段完成。


內存泄露

不再用到的內存,沒有及時釋放,就叫做內存泄漏(memory leak)。泄露的原因主要有緩存、閉包、全局變量、計時器中引用沒有清除等原因。

這裏我寫了一篇更詳細具體的文章,《Chrome 瀏覽器垃圾回收機制與內存泄漏分析》。

大家可以看一下,這裏就不詳細說了~


利用瀏覽器進行性能分析

這部分的內容,比較重要。我用了2篇文章來詳細說了。

大家可以看一下,這裏就不詳細說了~


參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章