前言
現代瀏覽器十分復雜,頗有運行在操作系統之上的"操作系統"的意思,我們將盡可能用簡單容易理解的例子來簡單概括它主要的工作邏輯。

目錄:
- 進程與線程概述;
- 瀏覽器架構;
- 瀏覽器視角下的輸入;
- 頁面如何渲染;
- 如何進行交互;
Part 1. 進程與線程概述
計算機的核心是 CPU,它承擔了幾乎所有的計算任務。

你可以把 CPU 想象成是一個工廠,時刻在運行着。
假設這個工廠的電力有限,同一時刻只能供一個車間使用。這也就意味着,一個車間正在使用,其他車間都將不會被使用。

進程就好比車間,是工廠將要執行的任務。潛台詞就是說,單個 CPU 任意時刻總是只能運行一個任務。
一個車間可以有很多的工人,它們協同完成同一個任務。

線程就是車間里的工人。
假設工人都是很耗電的機器人,靠着分得工廠給的電力進行任務,每一次給的電力剛好夠完成本次的任務,而工廠同一時刻又只能給一個機器人供電。

這幾乎就是單核 CPU 的工作方式了:同一時刻只能做一個工作。
但你仍然感覺到許多不同的任務正在 "同時" 運行着,這是因為當切換任務的速度足夠快時,你將感知不到 CPU 同一時刻只能做一個工作的特性:

我們的 CPU 就這樣飛速地奔騰着。
每當我們打開一個應用,就會啟動一個進程。程序也會創建一個或多個線程來幫助它完成工作。
操作系統會為進程提供一個可使用的 "一塊" 內存,就像開工廠占地一樣,所有應用程序的狀態信息都會保存在該私有內存空間中。程序關閉時,相應進程會消失,操作系統也會釋放內存。

進程可以請求操作系統啟動另一個進程來執行不同的任務。此時內存不同區域會分給新進程。
如果兩個進程需要對話,他們可以通過 進程間通信(IPC) 來進行。

許多應用程序就是這樣設計的,如果一個工作進程失去響應,該進程就可以在不停止應用程序的情況下靠着其他進程重新啟動。
Part 2. 瀏覽器架構
那么如何通過進程和線程構建 web 瀏覽器呢?

雖然對於如何構建 web 瀏覽器沒有明確的標准,但現在擁有一個導航欄、輸入框、標簽頁這樣類似的設計卻是不同瀏覽器之間默契的共同選擇。
瀏覽器的架構也總體分為兩類:

現在已經很難看到單進程的架構方式了,因為單進程的瀏覽器需要處理的事情太多(網絡、渲染、管理插件等),極不穩定和安全。因此市面上主流的瀏覽器都已經升級為多進程的方式。
就拿 Chrome 舉例來說,就采取了下方的架構方式:

- 最頂層是瀏覽器進程,負責協調處理其他進程模塊的任務。
- UI 進程負責控制地址欄、標簽頁等;
- 渲染進程控制標簽頁內網站的展示。
- 插件進程控制站點使用的任意插件,比如:Flash。
- GPU 進程單獨處理來自不同應用發送的繪制請求。
- ....
多進程的好處顯而易見。比如當你打開了三個標簽頁,其中一個崩潰了,你可以關掉它而不會影響其他兩個標簽頁:

並且由於進程的數據是私有的,所以一定程度上能夠保證安全性。
但缺點也顯而易見。我們上面用車間來類比進程,用工人來類比線程,顯然「建一座車間」比「招聘一個工人」消耗的資源要大得多——哪怕車間只有一個工人——這里比較明顯的是對內存的消耗。

為了避免過大的內存消耗,Chrome 把一些服務做了聚合:

這樣就能一定程度上減少內存的開銷。
Part 3. 瀏覽器視角下的輸入
當在瀏覽器中鍵入一個 URL 地址,瀏覽器會做什么處理呢?

第一步:處理輸入
我們已經習慣了一個鏈接打開就對應一個外部網站,但它還可能是瀏覽器本身的設置頁(如 chrome://settings/),或是本地硬盤的地址(如 Mac 下的 \):

所以我們的第一步就是要判斷這個輸入到底是個啥:

第二步:開始導航
隨着用戶輸入完畢按下 Enter 鍵,UI 線程知道要啟用網絡去調取網站的信息。網絡線程會負責聯系目標主機並獲取到信息:

網絡線程獲取信息的過程,發生了很多事,比如 DNS 域名解析、TLS 建立連接等,如果不熟悉可以看看之前的系列文章。
第三步:讀取響應
總之網絡線程為我們取到了來自網站的響應,大概長這樣:

響應分為 header 和 payload 兩個部分。header 類似於一本書的版權、作者介紹等相關信息,而 payload 才是真實的數據內容。
瀏覽器需要根據響應頭里的 Content-Type 來區分對應內容的類型,例如 text/html 時瀏覽器會對內容進行 HTML 解析,image/png 則調用圖片渲染器。
然而完全信任網站響應的 Content-Type 是不行的,因為一旦 Content-Type 未指定或者是一個錯誤的值的時候,就會發生未知的錯誤。
所以當收到響應主體(payload)時,網絡線程會在必要時檢查數據的前幾個字節,以確保數據內容與 header 里標識的數據類型(Content-Type)一致。如果不一致,那么就需要進行 MIME 類型嗅探來猜測該數據的類型。

當響應是一個 HTML 文件時,此時也會進行安全檢查(SafeBrowsing 檢查)。如果域名和相應數據似乎匹配到了一個已知的惡意網站,那么網絡線程會顯示一個警告頁面。
除此之外,還會發生 Cross Origin Read Blocking(CORB)檢查,以確保敏感的跨域數據不被傳給渲染進程。
第四步:查找渲染進程
一旦所有的檢查執行完畢並且網絡線程確信瀏覽器會導航到請求的站點,網絡線程會告訴 UI 線程所有的數據准備完畢。UI 線程會尋找渲染進程去開始渲染 web 頁面。

由於網絡請求會花費幾百毫秒才獲取回響應,因此可以應用一個優化措施。
當第 2 步 UI 線程正發送一個 URL 請求給網絡線程時,它已經知道它們會導航到哪個站點。在網絡請求的同時,UI 線程並行地嘗試主動尋找或開啟一個渲染進程。
這樣,如果一切按預期進行,渲染進程在網絡線程接受到數據時就已經處於待命狀態。
第五步:提交導航
現在數據和渲染進程已經就緒,瀏覽器進程會發送一個 IPC(進程間通信)到渲染進程去提交導航。

這時地址欄會更新、標簽頁的歷史記錄也會更新,前進/后退按鈕會走向剛導航過的站點。渲染進程根據 HTML 內容開始解析並渲染頁面。最終您將看到網站設計者設計的網站。
Part 4. 頁面如何渲染
渲染進程涉及 Web 性能的許多方面,流程非常復雜,我們只做必要的理解。如果您想要深入了解,可以在 web.dev 找到相關資源。

渲染進程內部包含主線程、工作線程、合成線程和光柵線程。
在詳細說明之前,請先想象一個這樣的場景:您站在一副簡單繪畫的面前,如何通過打電話來讓您的朋友知道這幅畫究竟長什么樣子呢?

如果您真打算這么做,這里參考 HTML 解析的過程給您提供一些建議。
首先,圖中的元素以及具體元素的屬性分開描述(如:圖里有一個圓是元素,圓有多大具體在什么位置等是屬性):

這樣做的好處是可閱讀性變高了,有哪些元素,以及元素哪些屬性一目了然,也利於分別維護和修改。(類似於書的目錄和對應內容一樣)

另外是你可以提煉一些通用的屬性來減少描述:

然后,最好是分層進行描述,因為圖畫是有層次的,光有元素大小、位置等信息是不夠的:

元素實際上就是我們通常說的 HTML 文件,HTML 文件中包含了描述元素屬性的 CSS 樣式文件。每個瀏覽器對應常見的樣式都會有默認的樣式。
瀏覽器實際上要知道繪制些什么元素,每個元素屬性如何是要分成三步的:1)通過 HTML 繪制元素樹(俗稱 DOM 樹);2)通過 CSS 文件繪制樣式樹(俗稱 CSSOM 樹);3)綜合兩顆樹繪制渲染樹(俗稱 Render Tree);

現在瀏覽器知道文檔的結構、每個元素的樣式、頁面的幾何形狀和繪制順序,它是如何繪制頁面的?把這些信息轉換為屏幕上的像素,我們稱為光柵化。

處理這種情況的一種簡單的方法是,先在光柵化視窗內的畫面,如果用戶滾動頁面,則移動光柵框,並光柵化填充缺少的部分。這就是 Chrome 首次發布時處理光柵化的方式。
但是,現代瀏覽器會運行一個更復雜的過程,我們稱為合成。

合成是一種將頁面的各個部分分層,分別光柵化,並在稱為合成線程的單獨線程中合成為頁面的技術。如果發生滾動,由於圖層已經光柵化,因此它所要做的只是合成一個新幀。動畫也可以以相同的方式(移動圖層和合成新幀)實現。
另外需要說明的是如何進行描述是有相當的技巧的。例如「正中心有一個 半徑為 2 的圓」和「正中心有一個 直徑為頁面寬度 50% 的圓」是完全不同的:

如何進行組織描述,這需要網站建設者的經驗。
Part 5. 如何進行交互
在瀏覽器眼中,用戶的一切行為都是輸入。不單單是滾動鼠標滑輪,或是點擊屏幕、按下按鍵等。

對於瀏覽器進程來說只存在事件和對應坐標,只有渲染進程知道頁面究竟長啥樣,以及究竟該如何處理事件。瀏覽器進程只負責把事件和坐標發送給渲染進程。
我們也可以編寫自己的邏輯文件(js 文件)來監聽某一事件進行對應的處理。然后再統一由渲染進程進行合成。為了瀏覽流暢,瀏覽器需要保證渲染進程的渲染速度與屏幕刷新率一致(大概每秒 60 幀)。

另外為了降低主線程中傳遞過量的調用,Chrome 也會把一些連續的事件進行合並。
瀏覽器進程監聽並發送事件給渲染進程進行渲染,這大概就是瀏覽器交互的基本方式。
后記
瀏覽器的復雜遠不是一篇文章能解釋清楚的,本篇文章也只是想讓大家理解瀏覽器的基本過程和原理。盡可能使用動圖的形式清晰地表達,希望大家能用餐愉快。

本文大量借鑒了 Chrome 官方 developer 分享的系列文章(下2),如果有想更加深入了解的小伙伴也可以閱讀更加硬核的瀏覽器工作原理揭秘文章(下4)
至此,我們對瀏覽器已經有了相當的了解了。后續也會繼續跟大家一起學習計算機網絡的基礎知識,也會嘗試着跟着后端學習路線圖的腳步跟着大家一起學習進階。

這里是我沒有三顆心臟,歡迎關注公眾號 wmyskxz,2021,與您在 Be Better 的路上共同成長!
參考資料
- 進程與線程的簡單解釋 - http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
- 轉載:現代瀏覽器內部揭秘 - https://hasaki.xyz/blog/2020-01-20-轉載現代瀏覽器內部揭秘/
- 深入淺出瀏覽器渲染原理 - https://blog.fundebug.com/2019/01/03/understand-browser-rendering/
- 瀏覽器工作原理幕后揭秘 - https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/
(完)
