简略图解:输入 url 到出现页面,浏览器做了什么?
應該有很多前端開發人員都思考過這么一個問題:從輸入 URL 到頁面加載完成,中間都做發生了什么?
這個問題涉及的面非常廣,每個涉及的點又很深入。從觸屏/鍵盤如何到 CPU?CPU 如何到系統內核?如何從操作系統 GUI 到瀏覽器?瀏覽器如何向網卡發送數據?數據如何從本機網卡發送到服務器?服務器接收數據后如何處理?服務器返回數據后瀏覽器如何處理?瀏覽器如何將頁面展現出來?等等等等,每一個過程都包含了大量且深入的知識體系,很難一以貫通。
但作為前端開發人員,瀏覽器是我們的主要工具之一,瀏覽器是如何將頁面展現出來的則是我們更關注的部分。因此本文就從一些基本流程來簡要描述這個過程。
從上面這個圖中可以發現,雖然使用的 Javascript 是單線程語言,但瀏覽器本身是多進程的。
但是這并不是從一而終的狀態,而是瀏覽器從早期的單進程結構逐漸發展發展而來。現代瀏覽器各進程根據負責的功能不同,分為瀏覽器進程、渲染器進程、網絡進程、GPU 進程、緩存進程、插件進程等等。為了更好的理解瀏覽器頁面的呈現過程,我們以最主流的 Chrome 為例,簡要的說明一下各個進程的大致職能:
-
瀏覽器進程: 負責控制界面展示、用戶交互、子進程管理等功能。
-
渲染器進程: 負責將 HTML\CSS\JS 轉化為用戶可以與之交互的網頁。渲染引擎如 webkit、blink 和 JS 引擎 V8 都是在該進程之中。
-
GPU 進程: GPU 進程原本是為了實現 3D CSS 效果,但是隨后頁面、Chrome 的 UI 都采用 GPU 來繪制,是 GPU 成為了重要需求,于是增加了 GPU 進程。
-
網絡進程: 負責頁面的網絡資源加載。
-
插件進程:負責插件的運行,由于插件可能崩潰,需要插件進程其他進程隔離。注意,插件并不是我們常用的瀏覽器拓展,plugin 和 extension 是不同的。
-
緩存進程:負責處理頁面資源緩存和清理。
我們本次需要重點關注的是渲染器進程。
回到問題,當我們在瀏覽器地址欄輸入地址時,瀏覽器進程的 UI 線程會捕捉輸入內容,如果訪問的是網址,那么 UI 線程會啟動一個網絡線程來構建請求(這里我們暫時不考慮緩存,緩存又是另外一個故事了),它請求 DNS 進行域名解析然后連接服務器獲取數據。如果我們輸入的是關鍵詞,瀏覽器則使用默認配置的搜索引擎來搜索。在獲取到數據并通過安全校驗后,網絡線程會通知 UI 線程數據準備完畢,然后UI線程創建一個渲染器進程來進行頁面的渲染,并將數據通過 IPC 管道傳遞給渲染器進程。
至此,我們的主角渲染器進程登場!
解析 HTML
渲染器進程接收到的是一個 HTML,需要把 HTML 解析成 DOM 數據結構。因為直接的 HTML 字節流是無法被渲染引擎所理解的,必須轉化成可以理解的內部結構。這個內部結構就是 DOM,DOM 提供了對 HTML 文檔的結構化表述。在渲染引擎中,DOM 有三個層面的作用:
-
從頁面角度:DOM 是生成頁面的基礎數據結構。
-
從 js 角度:DOM 提供了 js 操作的接口。通過這套接口,js 可以對 DOM 接口進行訪問,從而使開發者擁有改變文檔結構、樣式、內容的能力。
-
從安全角度:DOM 是 HTML 經過解析的內部數據結構,它將 web 頁面和 js 鏈接起來,并過濾了一些不安全的內容。
渲染器進程內部使用 HTML Parser 將 HTML 解析成 DOM 結構。需要注意的是,HTML 解析器不會等待整個 HTML 文檔加載完畢再去解析,而是加載多少了多少 HTML,就解析多少。
那么 HTML 字節流是如何轉換成 DOM 的呢?
其實和 V8 解析 js 類似,也是做詞法分析,通過分詞器將字節流成功成一個個 token,包括 Tag token 和文本 token。HTML 解析器維護了一個 token 棧結構,token 會按照對應順序入棧出棧,然后將 token 解析成 DOM 節點,并將 DOM 節點添加進 DOM 樹中。
前面提到生成 DOM 可以過濾一些不安全內容。這主要是渲染引擎中的一個名為XSSAuditor 安全檢查模塊實現的。它會監測詞法安全,在分詞器解析出 token 之后,檢查這些模塊是否引用了外部腳本,是否符合 CSP 規范,是否存在跨站點請求等。如果出現不符合規范的內容。XSSAuditor 會對該腳本或下載任務進行攔截。
DOM 樹在構建過程中會創建 document 對象,然后以 document 為根節點的 DOM 樹不斷修改向其中添加新的元素。
解析 CSS
前面已經將 HTML 解析成 DOM 樹了,但是光擁有 DOM 樹還不足以讓我們知道頁面的樣貌。因為我們肯定會為頁面設置一些樣式。因此主進程還會解析頁面中的 CSS 從而確定每個 DOM 節點的計算樣式(computed style)。
CSS 的樣式來源主要有三個:
-
通過 link 引用的外部 CSS 文件
-
使用
同樣,瀏覽器無法直接理解這些純文本的 CSS 樣式。所以渲染引擎在接受到 CSS 文本時,會通過 CSS parser 執行解析轉換操作。解析過程和 HTML 是部分類似的。最終將 CSS 文本轉換成瀏覽器可以理解的結構 styleSheets,這個結構具備查詢和修改的能力,為后續的樣式操作提供基礎。
然后將 styleSheet 中的屬性值進行標準化操作,比如我們在寫樣式時常常用到 font-size:1em、color:bule、font-weight:bold 等轉換成標準的計算值。
最后根據層疊樣式的繼承規則和層疊規則,計算出的每個 DOM 節點的樣式,被保存在 ComputedStyle 結構內。
渲染樹 Render Tree VS 布局樹 LayoutTree
到目前為止,我們已經在渲染器進程的主線程中走完了前兩步。我們已經有了節點,又知道了節點的樣式,是不是就可以開始渲染了 ?
不,進度條告訴我們事情遠沒有那么簡單。
但是在進行下一步之前,我們還需要厘清些概念。這其中 Layout Tree 我們是常聽的,那 Render Tree又是啥?它和 Layout Tree一樣嗎?
Layout Tree 不等于 Render Tree 。
從這篇開發者文檔[https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction?hl=zh-cn]中的配圖可以看到,Render tree 是將 dom 和 cssom 結合的產物。也就是主線程解析 CSS 并把計算后的樣式添加到 dom 節點上,進而得到了一個渲染樹。
The main thread parses CSS and determines the computed style for each DOM node. This is information about what kind of style is applied to each element based on CSS selectors.
———《Inside look at modern web browser(part 3)》
如圖所示,我們只是知道了節點是否可見和它們的可見樣式,但是還不知道節點的精確位置和大小。也就是需要進行布局。
主線程從 render tree 的根節點開始遍歷,按照一定規則處理后,將得到一個盒模型 。它會精確的捕獲每個元素在視口內的確切位置和尺寸,所有的相對測量值都會轉換為屏幕上的絕對元素。在得知了那些節點可見,計算樣式和幾何信息后,渲染引擎就可以把 render tree 上的每個節點都轉換成屏幕上的像素,這一步稱為 繪制 或者 柵格化
也就是說,Layout Tree 是 Render Tree 在進行布局計算后的結果,在 Render tree 的基礎上,增加了節點的幾何信息。
The main thread going over DOM tree with computed styles and producing layout tree ———《Inside look at modern web browser(part 3)》
圖層樹 Layer tree
真好,我們又走完一步,現在我們有了節點還有節點的精確位置和樣式,可不可以渲染了?
抱歉,還是不行。
這里我們要先了解一個概念,柵格化或者說光柵化(Restering)。簡單來說柵格化就是將這些節點信息轉化為屏幕上的像素點。
那么柵格化跟我們渲染有什么關系呢?因為瀏覽器使用的正是這個技術將元素繪制在屏幕上。
Chrome 以前是在可視區域內將元素柵格化,隨著用戶滾動頁面,不斷調整柵格化的區域,繼續柵格化并將內容填充到缺失部分效的方式。這樣的問題是用戶快速滾動頁面的時候,會出現卡頓感。
現在的 chrome 柵格化是采用一種合成(composting)的技術,把頁面中某些部分分到一些層中,分別柵格化它們,然后在柵格化線程中合成。這樣在頁面滾動時,原材料已經有了(已經柵格化好的那些層),只需要將視口內的層合成為一個新的幀就好了。
那這又跟 Layer Tree 又有啥關系呢?
前面說過目前 Chrome 使用的是將多個圖層合成為一幀的技術。Layer Tree的作用就是,分層。
為了找到那些元素應該在哪些層里,主線程遍歷 layout tree 來創建 layer tree (Chrome devtools 里稱為 ‘update layer tree’)
渲染引擎并不會為每個節點創建一個圖層,如果一個節點沒有圖層,那么它屬于父節點的圖層。想要創建新圖層,節點需要滿足一定條件。
- 擁有層疊上下文屬性的元素會被提成為新的一層
頁面是二維平面,但是層疊上下文會讓 HTML 元素具有三維概念。這些元素按照自身屬性的優先級分布在垂直于這個二維平面的 Z 軸上。
明確定位屬性的元素、定義透明屬性的元素、使用 CSS 濾鏡的元素等等,都擁有層疊上下文屬性。具體參考MDN[https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context]。
- 需要裁減的地方也會被創建為圖層
當我們在一個 100100 的 div 中書寫大量文字時,文字所顯示的區域肯定會超出 100100。這時候就產生了剪裁,渲染引擎會裁剪文字內容的一部分用于顯示在 div 區域。出現這種裁剪情況的時候,渲染引擎會為文字部分單獨創建一個層。如果出現滾動條,滾動條也會被提升為單獨的層。只要滿足上述 2 條件任意之一,即會被提升為單獨一層。
繪制 Paint
經歷了上述步驟,終于我們到了繪制這一步了。
繪制是其實一個大的過程,包括生成繪制記錄 Paint Records,合成器分圖塊,柵格線程光柵化(調用 GPU 生成位圖),合成器幀提交等過程。
通過分層我們知道了一些特殊元素的層級關系。但是,我們還不知道同一層內的元素的層級關系,誰該覆蓋誰。主線程根據前面的 Layer Tree 為每一層創建繪制記錄表 Paint Records,決定誰先畫誰后畫。后畫上去的肯定覆蓋前面的,也就決定了同一層內的元素層級。繪制記錄表也理解成一個類似單向鏈表的形式,遍歷鏈表即可獲得繪制順序。
在查看文檔的過程中,我們會發現不同文檔對先生成 Layer Tree 還是先得到 Paint Records 有不同說法。
我理解的應該是先分層,然后對每一層創建 Paint Records。如果是先遍歷整個 Layout tree 得到繪制記錄再分層的話,會多了很多額外的工作,比如把繪制記錄的某些繪制步驟挑出來和某些層綁定在一起。而且從 Chrome devtool 里的 profermance 可以看到,先創建了 layer tree,然后開始 paint。
有了圖層和繪制記錄表之后,將信息提交給合成器線程進行合成。由于一個圖層可能非常非常大,超過了視口的面積,那么一次性將這么大的圖層全繪制出來是沒有必要的。所以還需要將圖層分割成一個個圖塊 Tile, 優先繪制這些圖塊。圖塊大小通常是256256 或 512512,然后將圖塊信息傳遞給柵格化線程池。
柵格化線程池中都是柵格化線程,這些線程執行柵格任務 Raster Task 將圖塊生成位圖,優先生成視口 viewport 附近的位圖。通常柵格化過程使用 GPU 來加速,所以又稱為快速柵格化、GPU 柵格化。
當所有的圖塊柵格化完畢,合成器線程收集 Draw Quads 的圖塊信息。Draw Quads 記錄了圖塊在內存中的位置和在頁面哪個位置繪制圖塊。
現在萬事俱備,在主線程內將 Draw Quads 這些信息合成合成器幀 (Compositer Frame)并通過 IPC管道發送給瀏覽器進程。瀏覽器進程再將合成器幀發送給GPU。
GPU執行渲染,頁面出現!!!
大功告成!!!但是這不是結束,我們還要考慮到重排重繪。
從線程角度看重排重繪
作為前端經常聽到說重排比重繪的開銷大,那我們從線程角度該如何理解呢?
重排(回流)
如果通過 js 或 CSS 修改元素的幾何位置屬性,如寬度、高度等,那么瀏覽器會觸發重新布局。也就是重新生成 layout tree 及以后的所有流程,并全都再走一遍。這種開銷是比較大的。
重繪
如果只是改變元素背景顏色,則不用修改 layout tree 和 layer tree,也不用修改進入繪制以及之后的流程。由于省略了布局和分層階段,開銷會小一些,效率較高。
直接合成
如果更改一個即不要布局也不需要繪制的屬性,則渲染引擎將跳過布局和繪制階段,只執行后續的合成操作,我們把這個過程稱之為合成。
js 執行,重排,重繪都是運行在主線程的,都有可能因為大量的計算導致頁面卡頓。而除了主線程外,還有合成器線程和柵格線程,如果能不使用主線程直接進行合成的話,就能使頁面更加流暢。
css 3 transform 就是這樣的一個屬性,它實現動畫效果可以避開重排和重繪,直接在非主線程上執行動畫合成的操作。由于不占用主線程,并且也沒有布局和繪制的階段,所以效率是最高的。
另外,除了使用 transform 屬性外,還可以使用 requestAnimationFrame 方法。requestAnimationFrame 傳入的 callback 會在下一幀的重繪前調用,從而盡可能的提高動畫幀率。可以參考這篇文檔[https://zhuanlan.zhihu.com/p/64917985]。
延展閱讀
瀏覽器的演進
按照目前發展情況來,未來 Chrome 整體架構會朝向現代操作系統所采用的“面向服務的架構” 方向發展,以達到簡單、穩定、高速、安全的目標。
現有的各種模塊將重構成獨立的服務(Service),比如把 UI、數據庫、文件、設備、網絡等模塊重構為類似操作系統底層的基礎服務,并在各自獨立的進程中運行。同時通過使用定義好的接口以及 IPC 來通信、訪問,讓系統更內聚、松耦合、易于維護和擴展。
同時 Chrome 還提供靈活的彈性架構,在強大性能設備上以多進程的方式運行基礎服務,在資源受限的設備上(如下圖),則會將很多服務整合到一個進程中,從而節省內存占用。谷歌開發者文檔[https://developers.google.com/web/updates/2018/09/inside-browser-part1#at_the_core_of_the_computer_are_the_cpu_and_gpu]
目前 Chrome 正在逐步構建 Chrome 基礎服務(Chrome Foundation Service)這將是一個漫長的迭代過程,讓我們一起拭目以待。
參考文獻
Render-tree Construction,Layout,and Paint:
[https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction]
Inside look at modern web browser(par1 - part4):
[https://developers.google.com/web/updates/2018/09/inside-browser-part3]
Chrome 瀏覽器架構:
[https://xie.infoq.cn/article/5d36d123bfd1c56688e125ad3]
Chrome架構:僅僅打開了1個頁面,為什么有4個進程:
[https://blog.poetries.top/browser-working-principle/guide/part1/lesson01.html#%e8%bf%9b%e7%a8%8b%e5%92%8c%e7%ba%bf%e7%a8%8b]
requestAnimationFrame 回調時機:
[https://zhuanlan.zhihu.com/p/64917985]
層疊上下文:
[https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context]
Process Models:
[https://www.chromium.org/developers/design-documents/process-models]
總結
以上是生活随笔為你收集整理的简略图解:输入 url 到出现页面,浏览器做了什么?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: go-zero:微服务框架
- 下一篇: 非VIP用户下载限速,原来是这么实现的