天天看點

【Web技術】427- 圖解浏覽器的基本工作原理

可能每一個前端工程師都想要了解浏覽器的工作原理。

我們希望知道從在浏覽器位址欄中輸入 url 到頁面展現的短短幾秒内浏覽器究竟做了什麼;

我們希望了解平時常常聽說的各種代碼優化方案是究竟為什麼能起到優化的作用;

我們希望更細化的了解浏覽器的渲染流程。

浏覽器的多程序架構

一個好的程式常常被劃分為幾個互相獨立又彼此配合的子產品,浏覽器也是如此,以 Chrome 為例,它由多個程序組成,每個程序都有自己核心的職責,它們互相配合完成浏覽器的整體功能,每個程序中又包含多個線程,一個程序内的多個線程也會協同工作,配合完成所在程序的職責。

對一些前端開發同學來說,程序和線程的概念可能會有些模糊,為了更好的了解浏覽器的多程序架構,這裡我們簡單讨論一下程序和線程。

程序(process)和線程(thread)
【Web技術】427- 圖解浏覽器的基本工作原理

程序就像是一個有邊界的生産廠間,而線程就像是廠間内的一個個員工,可以自己做自己的事情,也可以互相配合做同一件事情。

當我們啟動一個應用,計算機會建立一個程序,作業系統會為程序配置設定一部分記憶體,應用的所有狀态都會儲存在這塊記憶體中,應用也許還會建立多個線程來輔助工作,這些線程可以共享這部分記憶體中的資料。如果應用關閉,程序會被終結,作業系統會釋放相關記憶體。更生動的示意圖如下:

程序(process)和線程(thread)

一個程序還可以要求作業系統生成另一個程序來執行不同的任務,系統會為新的程序配置設定獨立的記憶體,兩個程序之間可以使用 IPC (Inter Process Communication)進行通信。很多應用都會采用這樣的設計,如果一個工作程序反應遲鈍,重新開機這個程序不會影響應用其它程序的工作。

程序間通過 IPC 通信

如果對程序及線程的了解還存在疑惑,可以參考下述文章。

程序與線程的一個簡單解釋 - 阮一峰的網絡日志:http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html

浏覽器的架構

有了上面的知識做鋪墊,我們可以更合理的讨論浏覽器的架構了,其實如果要開發一個浏覽器,它可以是單程序多線程的應用,也可以是使用 IPC 通信的多程序應用。

【Web技術】427- 圖解浏覽器的基本工作原理

不同浏覽器的架構模型

不同浏覽器采用了不同的架構模式,這裡并不存在标準,本文以 Chrome 為例進行說明 :

Chrome 采用多程序架構,其頂層存在一個 Browser process 用以協調浏覽器的其它程序。

【Web技術】427- 圖解浏覽器的基本工作原理

Chrome 的不同程序

具體說來,Chrome 的主要程序及其職責如下:

  • Browser Process:
  1. 負責包括位址欄,書簽欄,前進後退按鈕等部分的工作;
  2. 負責處理浏覽器的一些不可見的底層操作,比如網絡請求和檔案通路;
  • Renderer Process:
  1. 負責一個 tab 内關于網頁呈現的所有事情
  • Plugin Process:
  1. 負責控制一個網頁用到的所有插件,如 flash
  • GPU Process
  1. 負責處理 GPU 相關的任務
【Web技術】427- 圖解浏覽器的基本工作原理

不同程序負責的浏覽器區域示意圖

Chrome 還為我們提供了「任務管理器」,供我們友善的檢視目前浏覽器中運作的所有程序及每個程序占用的系統資源,右鍵單擊還可以檢視更多類别資訊。

通過「頁面右上角的三個點點點 —- 更多工具 —- 任務管理器」即可打開相關面闆,

【Web技術】427- 圖解浏覽器的基本工作原理

Chrome 任務管理器面闆

Chrome 多程序架構的優缺點

優點

  1. 某一渲染程序出問題不會影響其他程序
  2. 更為安全,在系統層面上限定了不同程序的權限

缺點

由于不同程序間的記憶體不共享,不同程序的記憶體常常需要包含相同的内容。

為了節省記憶體,Chrome 限制了最多的程序數,最大程序數量由裝置的記憶體和 CPU 能力決定,當達到這一限制時,新打開的 Tab 會共用之前同一個站點的渲染程序。

Chrome 多程序架構的優缺點

測試了一下在 Chrome 中打開不斷打開知乎首頁,在 Mac i5 8g 上可以啟動四十多個渲染程序,之後新打開 tab 會合并到已有的渲染程序中。

Chrome 把浏覽器不同程式的功能看做服務,這些服務可以友善的分割為不同的程序或者合并為一個程序。以 Broswer Process 為例,如果 Chrome 運作在強大的硬體上,它會分割不同的服務到不同的程序,這樣 Chrome 整體的運作會更加穩定,但是如果 Chrome 運作在資源貧瘠的裝置上,這些服務又會合并到同一個程序中運作,這樣可以節省記憶體,示意圖如下。

不同性能的硬體的不同程序劃分

iframe 的渲染 — Site Isolation

在上面的程序圖中我們還可以看到一些程序下還存在着 Subframe,這就是 Site Isolation 機制作用的結果。

Site Isolation 機制從 Chrome 67 開始預設啟用。這種機制允許在同一個 Tab 下的跨站 iframe 使用單獨的程序來渲染,這樣會更為安全。

【Web技術】427- 圖解浏覽器的基本工作原理

iframe 會采用不同的渲染程序

Site Isolation 被大家看做裡程碑式的功能, 其成功實作是多年工程努力的結果。Site Isolation 不是簡單的疊加多個程序。這種機制在底層改變了 iframe 之間通信的方法,Chrome 的其它功能都需要做對應的調整,比如說 devtools 需要相應的支援,甚至 Ctrl + F 也需要支援。關于 Site Isolation 的更多内容可參考下述連結

​​https://developers.google.com/web/updates/2018/07/site-isolationdevelopers.google.com​​

介紹完了浏覽器的基本架構模式,接下來我們看看一個常見的導航過程對浏覽器來說究竟發生了什麼。

導航過程發生了什麼

也許大多數人使用 Chrome 最多的場景就是在位址欄輸入關鍵字進行搜尋或者輸入位址導航到某個網站,我們來看看浏覽器是怎麼看待這個過程的。

我們知道浏覽器 Tab 外的工作主要由 Browser Process 掌控,Browser Process 又對這些工作進一步劃分,使用不同線程進行處理:

  • UI thread :控制浏覽器上的按鈕及輸入框;
  • network thread: 處理網絡請求,從網上擷取資料;
  • storage thread: 控制檔案等的通路;
【Web技術】427- 圖解浏覽器的基本工作原理

浏覽器主程序中的不同線程

回到我們的問題,當我們在浏覽器位址欄中輸入文字,并點選回車獲得頁面内容的過程在浏覽器看來可以分為以下幾步:

處理輸入

UI thread 需要判斷使用者輸入的是 URL 還是 query;

開始導航

當使用者點選Enter鍵,UI thread 通知 network thread 擷取網頁内容,并控制 tab 上的 spinner 展現,表示正在加載中。

network thread 會執行 DNS 查詢,随後為請求建立 TLS 連接配接

【Web技術】427- 圖解浏覽器的基本工作原理

UI thread 通知 Network thread 加載相關資訊

如果 network thread 接收到了重定向請求頭如 301,network thread 會通知 UI thread 伺服器要求重定向,之後,另外一個 URL 請求會被觸發。

讀取響應

當請求響應傳回的時候,network thread 會依據 Content-Type 及 MIME Type sniffing 判斷響應内容的格式

【Web技術】427- 圖解浏覽器的基本工作原理

判斷響應内容的格式

如果響應内容的格式是 HTML ,下一步将會把這些資料傳遞給 renderer process,如果是 zip 檔案或者其它檔案,會把相關資料傳輸給下載下傳管理器。

Safe Browsing 檢查也會在此時觸發,如果域名或者請求内容比對到已知的惡意站點,network thread 會展示一個警告頁。此外 CORB 檢測也會觸發確定敏感資料不會被傳遞給渲染程序。

【Web技術】427- 圖解浏覽器的基本工作原理

查找渲染程序

當上述所有檢查完成,network thread 确信浏覽器可以導航到請求網頁,network thread 會通知 UI thread 資料已經準備好,UI thread 會查找到一個 renderer process 進行網頁的渲染。

【Web技術】427- 圖解浏覽器的基本工作原理

收到 Network thread 傳回的資料後,UI thread 查找相關的渲染程序

由于網絡請求擷取響應需要時間,這裡其實還存在着一個加速方案。當 UI thread 發送 URL 請求給 network thread 時,浏覽器其實已經知道了将要導航到那個站點。UI thread 會并行的預先查找和啟動一個渲染程序,如果一切正常,當 network thread 接收到資料時,渲染程序已經準備就緒了,但是如果遇到重定向,準備好的渲染程序也許就不可用了,這時候就需要重新開機一個新的渲染程序。

确認導航

進過了上述過程,資料以及渲染程序都可用了, Browser Process 會給 renderer process 發送 IPC 消息來确認導航,一旦 Browser Process 收到 renderer process 的渲染确認消息,導航過程結束,頁面加載過程開始。

此時,位址欄會更新,展示出新頁面的網頁資訊。history tab 會更新,可通過傳回鍵傳回導航來的頁面,為了讓關閉 tab 或者視窗後便于恢複,這些資訊會存放在硬碟中。

【Web技術】427- 圖解浏覽器的基本工作原理

Browser Process 和 Renderer Process 通過 IPC 通信,請求 Renderer Process 渲染頁面

額外的步驟

一旦導航被确認,renderer process 會使用相關的資源渲染頁面,下文中我們将重點介紹渲染流程。當 renderer process 渲染結束(渲染結束意味着該頁面内的所有的頁面,包括所有 iframe 都觸發了 onload 時),會發送 IPC 信号到 Browser process, UI thread 會停止展示 tab 中的 spinner。

【Web技術】427- 圖解浏覽器的基本工作原理

Renderer Process 發送 IPC 消息通知 browser process 頁面已經加載完成

當然上面的流程隻是網頁首幀渲染完成,在此之後,用戶端依舊可下載下傳額外的資源渲染出新的視圖。

在這裡我們可以明确一點,所有的 JS 代碼其實都由 renderer Process 控制的,是以在你浏覽網頁内容的過程大部分時候不會涉及到其它的程序。不過也許你也曾經監聽過 ​

​beforeunload​

​ 事件,這個事件再次涉及到 Browser Process 和 renderer Process 的互動,當目前頁面關閉時(關閉 Tab ,重新整理等等),Browser Process 需要通知 renderer Process 進行相關的檢查,對相關事件進行處理。

【Web技術】427- 圖解浏覽器的基本工作原理

浏覽器程序發送 IPC 消息給渲染程序,通知要離開目前網站了

如果導航由 renderer process 觸發(比如在使用者點選某連結,或者JS執行 ​

​window.location = http://newsite.com​

​​ ) renderer process 會首先檢查是否有 ​

​beforeunload​

​ 事件處理器,導航請求由 renderer process 傳遞給 Browser process

如果導航到新的網站,會啟用一個新的 render process 來處理新頁面的渲染,老的程序會留下來處理類似 ​

​unload​

​ 等事件。

關于頁面的生命周期,更多内容可參考 Page Lifecycle API 。

【Web技術】427- 圖解浏覽器的基本工作原理

浏覽器程序發送 IPC 消息到新的渲染程序通知渲染新的頁面,同時通知舊的渲染程序解除安裝

除了上述流程,有些頁面還擁有 Service Worker (服務工作線程),Service Worker 讓開發者對本地緩存及判斷何時從網絡上擷取資訊有了更多的控制權,如果 Service Worker 被設定為從本地 cache 中加載資料,那麼就沒有必要從網上擷取更多資料了。

值得注意的是 service worker 也是運作在渲染程序中的 JS 代碼,是以對于擁有 Service Worker 的頁面,上述流程有些許的不同。

當有 Service Worker 被注冊時,其作用域會被儲存,當有導航時,network thread 會在注冊過的 Service Worker 的作用域中檢查相關域名,如果存在對應的 Service worker,UI thread 會找到一個 renderer process 來處理相關代碼,Service Worker 可能會從 cache 中加載資料,進而終止對網絡的請求,也可能從網上請求新的資料。

【Web技術】427- 圖解浏覽器的基本工作原理

Service Worker 依據具體情形做處理

關于 Service Worker 的更多内容可參考 The Service Worker Lifecycle

如果 Service Worker 最終決定通過網上擷取資料,Browser 程序 和 renderer 程序的互動其實會延後資料的請求時間 。Navigation Preload 是一種與 Service Worker 并行的加速加載資源的機制,服務端通過請求頭可以識别這類請求,而做出相應的處理。

更多内容可參考:Speed up Service Worker with Navigation Preloads

渲染程序是如何工作的

渲染程序幾乎負責 Tab 内的所有事情,渲染程序的核心目的在于轉換 HTML CSS JS 為使用者可互動的 web 頁面。渲染程序中主要包含以下線程:

【Web技術】427- 圖解浏覽器的基本工作原理

渲染程序包含的線程

  1. 主線程 Main thread
  2. 工作線程 Worker thread
  3. 排版線程 Compositor thread
  4. 光栅線程 Raster thread

後文我們将逐漸介紹不同線程的職責,在此之前我們先看看渲染的流程

  1. 建構 DOM

當渲染程序接收到導航的确認資訊,開始接受 HTML 資料時,主線程會解析文本字元串為 DOM。

渲染 html 為 DOM 的方法由 HTML Standard 定義。

  1. 加載次級的資源

網頁中常常包含諸如圖檔,CSS,JS 等額外的資源,這些資源需要從網絡上或者 cache 中擷取。主程序可以在建構 DOM 的過程中會逐一請求它們,為了加速 preload scanner 會同時運作,如果在 html 中存在 ​

​<img>​

​​ ​

​<link>​

​ 等标簽,preload scanner 會把這些請求傳遞給 Browser process 中的 network thread 進行相關資源的下載下傳。

  1. JS 的下載下傳與執行

當遇到 ​

​<script>​

​​ 标簽時,渲染程序會停止解析 HTML,而去加載,解析和執行 JS 代碼,停止解析 html 的原因在于 JS 可能會改變 DOM 的結構(使用諸如 ​

​documwnt.write()​

​等API)。

不過開發者其實也有多種方式來告知浏覽器應對如何應對某個資源,比如說如果在​

​<script>​

​​ 标簽上添加了 ​

​async​

​​ 或 ​

​defer​

​ 等屬性,浏覽器會異步的加載和執行JS代碼,而不會阻塞渲染。更多的方法可參考 Resource Prioritization – Getting the Browser to Help You

  1. 樣式計算

僅僅渲染 DOM 還不足以獲知頁面的具體樣式,主程序還會基于 CSS 選擇器解析 CSS 擷取每一個節點的最終的計算樣式值。即使不提供任何 CSS,浏覽器對每個元素也會有一個預設的樣式。

【Web技術】427- 圖解浏覽器的基本工作原理

渲染程序主線程計算每一個元素節點的最終樣式值

  1. 擷取布局

想要渲染一個完整的頁面,除了獲知每個節點的具體樣式,還需要獲知每一個節點在頁面上的位置,布局其實是找到所有元素的幾何關系的過程。其具體過程如下:

通過周遊 DOM 及相關元素的計算樣式,主線程會建構出包含每個元素的坐标資訊及盒子大小的布局樹。布局樹和 DOM 樹類似,但是其中隻包含頁面可見的元素,如果一個元素設定了 ​

​display:none​

​ ,這個元素不會出現在布局樹上,僞元素雖然在 DOM 樹上不可見,但是在布局樹上是可見的。

【Web技術】427- 圖解浏覽器的基本工作原理

主線程周遊 DOM 及 對應元素的樣式,建構出布局樹

  1. 繪制各元素

即使知道了不同元素的位置及樣式資訊,我們還需要知道不同元素的繪制先後順序才能正确繪制出整個頁面。在繪制階段,主線程會周遊布局樹以建立繪制記錄。繪制記錄可以看做是記錄各元素繪制先後順序的筆記。

【Web技術】427- 圖解浏覽器的基本工作原理

主線程依據布局樹建構繪制記錄

  1. 合成幀

熟悉 PS 等繪圖軟體的童鞋肯定對圖層這一概念不陌生,現代 Chrome 其實利用了這一概念來組合不同的層。

複合是一種分割頁面為不同的層,并單獨栅格化,随後組合為幀的技術。不同層的組合由 compositor 線程(合成器線程)完成。

主線程會周遊布局樹來建立層樹(layer tree),添加了 ​

​will-change​

​ CSS 屬性的元素,會被看做單獨的一層,

【Web技術】427- 圖解浏覽器的基本工作原理

主線程周遊布局樹生成層樹

你可能會想給每一個元素都添加上 ​

​will-change​

​,不過組合過多的層也許會比在每一幀都栅格化頁面中的某些小部分更慢。為了更合理的使用層,可參考 堅持僅合成器的屬性和管理層計數 。

一旦層樹被建立,渲染順序被确定,主線程會把這些資訊通知給合成器線程,合成器線程會栅格化每一層。有的層的可以達到整個頁面的大小,是以,合成器線程将它們分成多個磁貼,并将每個磁貼發送到栅格線程,栅格線程會栅格化每一個磁貼并存儲在 GPU 顯存中。

【Web技術】427- 圖解浏覽器的基本工作原理

栅格線程會栅格化每一個磁貼并存儲在 GPU 顯存中

一旦磁貼被光栅化,合成器線程會收集稱為繪制四邊形的磁貼資訊以建立合成幀。

合成幀随後會通過 IPC 消息傳遞給浏覽器程序,由于浏覽器的 UI 改變或者其它拓展的渲染程序也可以添加合成幀,這些合成幀會被傳遞給 GPU 用以展示在螢幕上,如果滾動發生,合成器線程會建立另一個合成幀發送給 GPU。

【Web技術】427- 圖解浏覽器的基本工作原理

合成器線程會發送合成幀給 GPU 渲染

合成器的優點在于,其工作無關主線程,合成器線程不需要等待樣式計算或者 JS 執行,這就是為什麼合成器相關的動畫 最流暢,如果某個動畫涉及到布局或者繪制的調整,就會涉及到主線程的重新計算,自然會慢很多。

浏覽器對事件的處理

浏覽器通過對不同僚件的處理來滿足各種互動需求,這一部分我們一起看看從浏覽器的視角,事件是什麼,在此我們先主要考慮滑鼠事件。

在浏覽器的看來,使用者的所有手勢都是輸入,滑鼠滾動,懸置,點選等等都是。

當使用者在螢幕上觸發諸如 touch 等手勢時,首先收到手勢資訊的是 Browser process, 不過 Browser process 隻會感覺到在哪裡發生了手勢,對 tab 内内容的處理是還是由渲染程序控制的。

事件發生時,浏覽器程序會發送事件類型及相應的坐标給渲染程序,渲染程序随後找到事件對象并執行所有綁定在其上的相關事件處理函數。

【Web技術】427- 圖解浏覽器的基本工作原理

事件從浏覽器程序傳送給渲染程序

前文中,我們提到過合成器可以獨立于主線程之外通過合成栅格化層平滑的處理滾動。如果頁面中沒有綁定相關事件,組合器線程可以獨立于主線程建立組合幀。如果頁面綁定了相關事件處理器,主線程就不得不出來工作了。這時候合成器線程會怎麼處理呢?

這裡涉及到一個專業名詞「了解非快速滾動區域(non-fast scrollable region)」由于執行 JS 是主線程的工作,當頁面合成時,合成器線程會标記頁面中綁定有事件處理器的區域為 non-fast scrollable region ,如果存在這個标注,合成器線程會把發生在此處的事件發送給主線程,如果事件不是發生在這些區域,合成器線程則會直接合成新的幀而不用等到主線程的響應。

【Web技術】427- 圖解浏覽器的基本工作原理

涉及 non-fast scrollable region 的事件,合成器線程會通知主線程進行相關處理

web 開發中常用的事件處理模式是事件委托,基于事件冒泡,我們常常在最頂層綁定事件:

document.body.addEventListener('touchstart', 
event => {    if (event.target === area) {        event.preventDefault();    }
}
);      

上述做法很常見,但是如果從浏覽器的角度看,整個頁面都成了 non-fast scrollable region 了。

這意味着即使操作的是頁面無綁定事件處理器的區域,每次輸入時,合成器線程也需要和主線程通信并等待回報,流暢的合成器獨立處理合成幀的模式就失效了。

【Web技術】427- 圖解浏覽器的基本工作原理

由于事件綁定在最頂部,整個頁面都成為了 non-fast scrollable region

為了防止這種情況,我們可以為事件處理器傳遞 ​

​passive: true​

​ 做為參數,這樣寫就能讓浏覽器即監聽相關事件,又讓組合器線程在等等主線程響應前建構新的組合幀。

document.body.addEventListener('touchstart', 
event => {    if (event.target === area) {        event.preventDefault()    }
}, {passive: true}
);      

不過上述寫法可能又會帶來另外一個問題,假設某個區域你隻想要水準滾動,使用 ​

​passive: true​

​​ 可以實作平滑滾動,但是垂直方向的滾動可能會先于​

​event.preventDefault()​

​​發生,此時可以通過 ​

​event.cancelable​

​ 來防止這種情況。

document.body.addEventListener('pointermove', event => {    if (event.cancelable) {        event.preventDefault(); // block the native scroll        /*
        *  do what you want the application to do here        */    } }, {passive: true});      

也可以使用css屬性 ​

​touch-action​

​ 來完全消除事件處理器的影響,如:

#area {  touch-action: pan-x; }      

查找到事件對象

當組合器線程發送輸入事件給主線程時,主線程首先會進行命中測試(hit test)來查找對應的事件目标,命中測試會基于渲染過程中生成的繪制記錄( paint records )查找事件發生坐标下存在的元素。

【Web技術】427- 圖解浏覽器的基本工作原理

主線程依據繪制記錄查找事件相關元素

事件的優化

一般我們螢幕的重新整理速率為 60fps,但是某些事件的觸發量會不止這個值,出于優化的目的,Chrome 會合并連續的事件(如 wheel, mousewheel, mousemove, pointermove, touchmove ),并延遲到下一幀渲染時候執行 。

而如 keydown, keyup, mouseup, mousedown, touchstart, 和 touchend 等非連續性事件則會立即被觸發。

【Web技術】427- 圖解浏覽器的基本工作原理

Chrome 會合并連續事件到下一幀觸發

合并事件雖然能提示性能,但是如果你的應用是繪畫等,則很難繪制一條平滑的曲線了,此時可以使用 ​

​getCoalescedEvents​

​ API 來擷取組合的事件。示例代碼如下:

window.addEventListener('pointermove', event => {    const events = event.getCoalescedEvents();    for (let event of events) {        const x = event.pageX;        const y = event.pageY;        // draw a line using x and y coordinates.    }
});      
【Web技術】427- 圖解浏覽器的基本工作原理

通過 getCoalescedEvents API 擷取到每一個事件

花了好久來整理上面的内容,整理的過程收獲還挺大的,也希望這篇筆記能對你有所啟發,如果有任何疑問,歡迎一起來讨論。

相關參考連結

  • CPU, GPU, Memory, and multi-process architecture
  • What happens in navigation
  • Inner workings of a Renderer Process
  • Input is coming to the Compositor
  • 浏覽器的工作原理:新式網絡浏覽器幕後揭秘

作者:@zhangwang

繼續閱讀