當下瀏覽器內核主要有 Webkit、Blink 等。本文分析注意是自 2001 年 Webkit 從 KHTML 分離出去并開源后,各大瀏覽器廠商魔改 Webkit 的時期,這些魔改的內核最終以 Chromium 受眾最多而脫穎而出。本文就以 Chromium 瀏覽器架構為基礎,逐層探入進行剖析。
這里以一個面試中最常見的題目從 URL 輸入到瀏覽器渲染頁面發(fā)生了什么?
開始。
這個很常見的題目,涉及的知識非常廣泛。大家可先從瀏覽器監(jiān)聽用戶輸入開始,瀏覽器解析 url 的部分,分析出應用層協(xié)議 是 HTTPS 還是 HTTP 來決定是否經(jīng)過會話層 TLS 套接字,然后到 DNS 解析獲取 IP,建立 TCP 套接字池 以及 TCP 三次握手,數(shù)據(jù)封裝切片的過程,瀏覽器發(fā)送請求獲取對應數(shù)據(jù),如何解析 HTML,四次揮手等等等等。 這個回答理論上可以非常詳細,遠比我提到的多得多。
本文試圖從瀏覽器獲取資源開始探究 Webkit。如瀏覽器如何獲取資源,獲取資源時 Webkit 調用了哪些資源加載器(不同的資源使用不同的加載器),Webkit 如何解析 HTML 等入手。想要從前端工程師的角度弄明白這些問題,可以先暫時拋開 C++源碼,從瀏覽器架構出發(fā),做到大致了解。之后學有余力的同學再去深入研究各個底層細節(jié)。
本文的路線循序漸進,從 Chromium 瀏覽器架構出發(fā),到 Webkit 資源下載時對應的瀏覽器獲取對應資源如 HTML、CSS 等,再到 HTML 的解析,再到 JS 阻塞 DOM 解析而產(chǎn)生的 Webkit 優(yōu)化 引出瀏覽器多線程架構,繼而出于安全性和穩(wěn)定性的考慮引出瀏覽器多進程架構。
(Chromium 瀏覽器架構)
我們通常說的瀏覽器內核,指的是渲染引擎。
WebCore 基本是共享的,只是在不同瀏覽器中使用 Webkit 的實現(xiàn)方式不同。它包含解析 HTML 生成 DOM、解析 CSS、渲染布局、資源加載器等等,用于加載和渲染網(wǎng)頁。
JS 解析可以使用 JSCore 或 V8 等 JS 引擎。我們熟悉的谷歌瀏覽器就是使用 V8。比如比較常見的有內置屬性 [[scope]]
就僅在 V8 內部使用,用于對象根據(jù)其向上索引自身不存在的屬性。而對外暴露的 API,如 __proto__
也可用于更改原型鏈。實際上 __proto__
并不是 ES 標準提供的,它是瀏覽器提供的(瀏覽器可以不提供,因此如果有瀏覽器不提供的話這也并不是 b ug)。
Webkit Ports 是不共享的部分。它包含視頻、音頻、圖片解碼、硬件加速、網(wǎng)絡棧等等,常用于移植。
同時,瀏覽器是多進程多線程架構,稍后也會細入。
在解析 HTML 文檔之前,需要先獲取資源,那么資源的獲取在 Webkit 中應該如何進行呢?
HTTP 是超文本傳輸協(xié)議,超文本的含義即包含了文本、圖片、視頻、音頻等等。其對應的不同文件格式,在 Webkit 中 需要調用不同的資源加載器,即 特定資源加載器。
而瀏覽器有四級緩存,Disk Cache 是我們最常說的通過 HTTP Header 去控制的,比如強緩存、協(xié)商緩存。同時也有瀏覽器自帶的啟發(fā)式緩存。而 Webkit 對應使用的加載器是資源緩存機制的資源加載器 CachedResoureLoader
類。
如果每個資源加載器都實現(xiàn)自己的加載方法,則浪費內存空間,同時違背了單一職責的原則,因此可以抽象出一個共享類,即通用資源加載器 ResoureLoader
類。 Webkit 資源加載是使用了三類加載器:特定資源加載器,資源緩存機制的資源加載器 CachedResoureLoader 和 通用資源加載器 ResoureLoader。
既然說到了緩存,那不妨多談一點。
資源既然緩存了,那是如何命中的呢?答案是根據(jù)資源唯一性的特征 URL。資源存儲是有一定有效期的,而這個有效期在 Webkit 中采用的就是 LRU 算法。那什么時候更新緩存呢?答案是不同的緩存類型對應不同的緩存策略。我們知道緩存多數(shù)是利用 HTTP 協(xié)議減少網(wǎng)絡負載的,即強緩存、協(xié)商緩存。但是如果關閉緩存了呢? 比如 HTTP/1.0 Pragma:no-cache 和 HTTP/1.1 Cache-Control: no-cache。此時,對于 Webkit 來說,它會清空全局唯一的對象 MemoryCache 中的所有資源。
資源加載器內容先到這里。瀏覽器架構是多進程多線程的,其實多線程可以直接體現(xiàn)在資源加載的過程中,在 JS 阻塞 DOM 解析中發(fā)揮作用,下面我們詳細講解一下。
瀏覽器是多進程多線程架構。
對于瀏覽器來講,從網(wǎng)絡獲取資源是非常耗時的。從資源是否阻塞渲染的角度,對瀏覽器而言資源僅分為兩類:阻塞渲染如 JS 和 不阻塞渲染如圖片。
我們都知道 JS 阻塞 DOM 解析,反之亦然。然而對于阻塞,Webkit 不會傻傻等著浪費時間,它在內部做了優(yōu)化:啟動另一個線程,去遍歷后續(xù)的 HTML 文檔,收集需要的資源 URL,并發(fā)下載資源。最常見的比如<script async>
和<script defer>
,其 JS 資源下載和 DOM 解析是并行的,JS 下載并不會阻塞 DOM 解析。這就是瀏覽器的多線程架構。
總結一下,多線程的好處就是,高響應度,UI 線程不會被耗時操作阻塞而完全阻塞瀏覽器進程。
關于多線程,有 GUI 渲染線程,負責解析 HTML、CSS、渲染和布局等等,調用 WebCore 的功能。JS 引擎線程,負責解析 JS 腳本,調用 JSCore 或 V8。我們都知道 JS 阻塞 DOM 解析,這是因為 Webkit 設計上 GUI 渲染線程和 JS 引擎線程的執(zhí)行是互斥的。如果二者不互斥,假設 JS 引擎線程清空了 DOM 樹,在 JS 引擎線程清空的過程中 GUI 渲染線程仍繼續(xù)渲染頁面,這就造成了資源的浪費。更嚴重的,還可能發(fā)生各種多線程問題,比如臟數(shù)據(jù)等。
另外我們常說的 JS 操作 DOM 消耗性能,其實有一部分指的就是 JS 引擎線程和 GUI 渲染線程之間的通信,線程之間比較消耗性能。
除此之外還有別的線程,比如事件觸發(fā)線程,負責當一個事件被觸發(fā)時將其添加到待處理隊列的隊尾。
值得注意的是,多啟動的線程,僅僅是收集后續(xù)資源的 URL,線程并不會去下載資源。該線程會把下載的資源 URL 送給 Browser 進程,Browser 進程調用網(wǎng)絡棧去下載對應的資源,返回資源交由 Renderer 進程進行渲染,Renderer 進程將最終的渲染結果返回 Browser 進程,由 Browser 進程進行最終呈現(xiàn)。這就是瀏覽器的多進程架構。
多進程加載資源的過程是如何的呢?我們上面說到的 HTML 文檔在瀏覽器的渲染,是交由 Renderer 進程的。Renderer 進程在解析 HTML 的過程中,已搜集到所有的資源 URL,如 link CSS、Img src 等等。但出于安全性和效率的角度考慮,Renderer 進程并不能直接下載資源,它需要通過進程間通信將 URL 交由 Browser 進程,Browser 進程有權限調用 URLRequest 類從網(wǎng)絡或本地獲取資源。
近年來,對于有的瀏覽器,網(wǎng)絡棧由 Browser 進程中的一個模塊,變成一個單獨的進程。
同時,多進程的好處遠遠不止安全這一項,即沙箱模型。還有單個網(wǎng)頁或者第三方插件的崩潰,并不會影響到瀏覽器的穩(wěn)定性。資源加載完成,對于 Webkit 而言,它需要調用 WebCore 對資源進行解析。那么我們先看下 HTML 的解析。之后我們再談一下,對于瀏覽器來說,它擁有哪些進程呢?
對于 Webkit 而言,將解析半結構化的 HTML 生成 DOM,但是對于 CSS 樣式表的解析,嚴格意義 CSSOM 并不是樹,而是一個映射表集合。我們可以通過 document.styleSheets 來獲取樣式表的有序集合來操作 CSSOM。對于 CSS,Webkit 也有對應的優(yōu)化策略---ComputedStyle。ComputedStyle 就是如果多個元素的樣式可以不經(jīng)過計算就確認相等,那么就僅會進行一次樣式計算,其余元素僅共享該 ComputedStyle。
共享 ComputedStyle 原則:
(1) TagName 和 Class 屬性必須一樣。
(2)不能有 Style。
(3)不能有 sibling selector。
(4)mappedAttribute 必須相等。
對于 DOM 和 CSSOM,大家說的合成的 render 樹在 Webkit 而言是不存在的,在 Webkit 內部生成的是 RenderObject,在它的節(jié)點在創(chuàng)建的同時,會根據(jù)層次結構創(chuàng)建 RenderLayer 樹,同時構建一個虛擬的繪圖上下文,生成可視化圖像。這四個內部表示結構會一直存在,直到網(wǎng)頁被銷毀。
RenderLayer 在瀏覽器控制臺中 Layers 功能卡中可以看到當前網(wǎng)頁的圖層分層。圖層涉及到顯式和隱式,如 scale()、z-index 等。層的優(yōu)點之一是只重繪當前層而不影響其他層,這也是 Webkit 做的優(yōu)化之一。同時 V8 引擎也做了一些優(yōu)化,比如說隱藏類、優(yōu)化回退、內聯(lián)緩存等等。
瀏覽器進程包括 Browser 進程、Renderer 進程、GPU 進程、NPAPI 插件進程、Pepper 進程等等。下面讓我們詳細看看各大進程。
注意:如果頁面有 iframe,它會形成影子節(jié)點,會運行在單獨的進程中。
我們僅僅在圍繞 Chromium 瀏覽器來說上述進程,因為在移動端,畢竟手機廠商很多,各大廠商對瀏覽器進程的支持也不一樣。這其實也是我們最常見的 H5 兼容性問題,比如 IOS margin-bottom
失效等等。再比如 H5 使用 video 標簽做直播,也在不同手機之間會存在問題。有的手機直播頁面跳出主進程再回來,就會黑屏。
以 Chromium 的 Android 版為例子,不存在 GPU 進程,GPU 進程變成了 Browser 進程的線程。同時,Renderer 進程演變?yōu)榉者M程,同時被限制了最大數(shù)量。
為了方便起見,我們以 PC 端谷歌瀏覽器為例子,打開任務管理器,查看當前瀏覽器中打開的網(wǎng)頁及其進程。
當前我打開了 14 個網(wǎng)頁,不太好容易觀察,但可以從下圖中看到,只有一個 Browser 進程,即第 1 行。但是打開的網(wǎng)頁對應的 Renderer 進程,并不一定是一個網(wǎng)頁對應一個 Renderer 進程,這跟 Renderer 進程配置有關系。比如你看第 6、7 行是每個標簽頁創(chuàng)建獨立 Renderer 進程,但是藍色光標所在的第 8、9、10 行是共用一個 Renderer 進程,這屬于為每個頁面創(chuàng)建一個 Renderer 進程。因為第 9、10 行打開的頁面是從第 8 行點擊鏈接打開的。第 2 行的 GPU 進程也清晰可見,以及第 3、4、5 行的插件進程。
關于,Renderer 進程和打開的網(wǎng)頁并不一定是一一對應的關系,下面我們詳細說一下 Renderer 進程。當前只有四種多進程策略:
Single process 突然讓我聯(lián)想到零幾年的時候,那會 IE 應該還是單進程瀏覽器。單進程就是指所有的功能模塊全部運行在一個進程,就類似于 Single process。那會玩 4399 如果一個網(wǎng)頁卡死了,沒響應,點關閉等一會,整個瀏覽器就崩潰了,得重新打開。所以多進程架構是有利于瀏覽器的穩(wěn)定性的。雖然當下瀏覽器架構為多進程架構,但如果 Renderer 進程配置為 Process-per-site-instance,也可能會出現(xiàn)由于單個頁面卡死而導致所有頁面崩潰的情況。
故瀏覽器多進程架構綜上所述,好處有三:
(1)單個網(wǎng)頁的崩潰不會影響這個瀏覽器的穩(wěn)定性。
(2)第三方插件的崩潰不會影響瀏覽器的穩(wěn)定性。
(3)沙箱模型提供了安全保障。
Webkit 使用三類資源加載器去下載對應的資源,并存入緩存池中,對于 HTML 文檔的解析,在阻塞時調用另一個線程去收集后續(xù)資源的 URL,將其發(fā)送給 Browser 進程,Browser 進程調用網(wǎng)絡棧去下載對應的本地或網(wǎng)絡資源,返回給 Renderer 進程進行渲染,Renderer 進程將最終渲染結果(一系列的合成幀)發(fā)送給 Browser 進程,Browser 進程將這些合成幀發(fā)送給 GPU 從而顯示在屏幕上。
(文中有部分不嚴謹?shù)牡胤?,已?lucifer 指出修改)
大家也可以關注我的公眾號《腦洞前端》獲取更多更新鮮的前端硬核文章,帶你認識你不知道的前端。