圖解瀏覽器
讀完需要5分鐘,速讀僅需3分鐘
這是前端食堂的第 56 篇原創(chuàng)
美味值:??????????
口味:仔梅燒小排

本文同步視頻版
01 瀏覽器架構(gòu)演進(jìn)
開篇我們先來簡單回顧下歷史,從 1993 年發(fā)布的第一款“好用”的瀏覽器 Mosaic,到 1994 年網(wǎng)景公司推出的紅極一時的 Navigator 瀏覽器,圖形用戶界面化的瀏覽器終于開始推動了 Web 技術(shù)的普及和發(fā)展。
微軟也隨后推出了 IE,加入戰(zhàn)場并取得瀏覽器大戰(zhàn)“一戰(zhàn)”的勝利。戰(zhàn)敗的網(wǎng)景公司索性將 Navigator 源代碼開源,創(chuàng)建了 Mozilla 基金會,并于 2004 年發(fā)布了 Firefox 瀏覽器。
蘋果公司于 2003 年發(fā)布了 Safari 瀏覽器,Google 公司于 2008 年發(fā)布了 Chrome 瀏覽器。Chrome 瀏覽器在瀏覽器大戰(zhàn)的“二戰(zhàn)”中技壓群雄,拔得頭籌?,F(xiàn)如今也是前端工程師最喜愛的瀏覽器,沒有之一。
Chrome 瀏覽器從 2007 年以前的單進(jìn)程架構(gòu)到現(xiàn)在的多進(jìn)程架構(gòu),瀏覽器的架構(gòu)在不斷的升級,變得更加穩(wěn)定、更加流暢、更加安全。目前 Chrome 的瀏覽器包括如下進(jìn)程:
1 個瀏覽器(Browser)主進(jìn)程 1 個 GPU 進(jìn)程 1 個網(wǎng)絡(luò)(NetWork)進(jìn)程 多個渲染進(jìn)程(運(yùn)行在沙箱模式下) 多個插件進(jìn)程

不過,軟件工程可沒有銀彈。瀏覽器的架構(gòu)體系也隨著調(diào)整變得更加復(fù)雜,也會有更高的資源占用。
那么如何尋求一種在資源占用和復(fù)雜架構(gòu)體系之間的平衡便成為了一個難題。
小孩子才做選擇,魚和熊掌我都要!
Chrome 團(tuán)隊在 2016 年使用“面向服務(wù)的架構(gòu)”(Services Oriented Architecture,簡稱 SOA)的思想設(shè)計了新的 Chrome 架構(gòu)。
他們將模塊重構(gòu)成獨(dú)立的服務(wù)(Service),服務(wù)運(yùn)行在獨(dú)立的進(jìn)程中,想要訪問的話必須使用定義好的接口,通過 IPC 來進(jìn)行通信。這樣的架構(gòu)無疑更加內(nèi)聚、松耦合、易于維護(hù)和擴(kuò)展。

02 瀏覽器導(dǎo)航渲染流程
從輸入 URL 到頁面展示,這中間發(fā)生了什么?
這是一道十分常見的面試題,不過大多數(shù)人回答這個問題時都不夠系統(tǒng)和全面,可見這道題能夠充分考察應(yīng)試者的知識深度。
我畫了一張圖整理了瀏覽器的導(dǎo)航渲染流程,下面我們來一起查缺補(bǔ)漏。

導(dǎo)航流程
用戶在地址欄輸入內(nèi)容后,地址欄會將輸入的內(nèi)容進(jìn)行合成 URL。 當(dāng)用戶輸入完內(nèi)容并按下回車鍵時,瀏覽器會在當(dāng)前頁面執(zhí)行 beforeunload 事件,你可以在這個鉤子中詢問是否要離開當(dāng)前頁面,常見于一些表單提交的場景。 接下來開始導(dǎo)航流程,瀏覽器進(jìn)入加載狀態(tài)。 瀏覽器的網(wǎng)絡(luò)進(jìn)程會先查找緩存中是否存在該資源,有的話直接返回,如果沒有的話會發(fā)起 URL 請求。 接下來首先要進(jìn)行的是 DNS 解析,獲得請求域名的服務(wù)器的 IP 地址(這個過程我也畫了一張圖,放在下文),如果協(xié)議是 HTTPS,還需要建立 TLS 連接。 接著利用目標(biāo)服務(wù)器的 IP 地址建立 TCP 連接(三次握手),構(gòu)建 HTTP 請求報文,發(fā)起請求。服務(wù)器收到請求后,會根據(jù)請求信息生成響應(yīng)報文。 瀏覽器的網(wǎng)絡(luò)進(jìn)程接收到響應(yīng)報文后進(jìn)行解析,如果狀態(tài)碼是 301 或者 302,則需要取得響應(yīng)頭中的 Location 對應(yīng)的地址進(jìn)行重定向,再重新發(fā)起請求。 如果狀態(tài)碼是 200,瀏覽器會根據(jù)響應(yīng)頭中的 Content-Type 字段來識別返回的響應(yīng)體數(shù)據(jù)類型,從而進(jìn)行不同的流程。如 text/html 代表 html 格式, application/octet-stream 代表字節(jié)流類型,瀏覽器會按照下載類型來處理。 如果是 HTML,瀏覽器會遵循 process-per-site-instance 默認(rèn)策略準(zhǔn)備渲染進(jìn)程,準(zhǔn)備好后就提交文檔(將網(wǎng)絡(luò)進(jìn)程接收到的數(shù)據(jù)提交給渲染進(jìn)程)。文檔被提交后,渲染進(jìn)程便開始進(jìn)行頁面解析和子資源的加載。
(當(dāng)然在第 7 點(diǎn)中還有 300、303 等 3xx 的狀態(tài)碼,具體含義可以參考我的這一篇專欄 那些年與面試官交手過的HTTP問題)
process-per-site-instance 默認(rèn)策略:每個標(biāo)簽對應(yīng)一個渲染進(jìn)程,如果從一個頁面打開了一個新頁面,新打開的頁面與當(dāng)前頁面還屬于同一個站點(diǎn)的話,那么新頁面會復(fù)用當(dāng)前頁面的渲染進(jìn)程。
渲染流程
渲染流程在上圖中一并畫了出來,需要經(jīng)過以下幾個階段:
構(gòu)建 DOM 樹 樣式計算 布局 分層 繪制 分塊 光柵化 合成
因為渲染流程的內(nèi)容比較多,本文先不詳細(xì)展開,后面我們再開一篇專欄進(jìn)行講解。
DNS
DNS 的解析是一個遞歸流程,順序如下圖中數(shù)字標(biāo)記所示:

根 DNS 服務(wù)器:返回頂級域 DNS 服務(wù)器的 IP 地址 頂級 DNS 服務(wù)器:返回權(quán)威 DNS 服務(wù)器的 IP 地址 權(quán)威 DNS 服務(wù)器:返回相應(yīng)主機(jī)的 IP 地址
03 垃圾回收
棧中的垃圾數(shù)據(jù)
先來看一段簡單的示例代碼:
function hello () {
var name = '前端食堂'
var food = { name: '回鍋肉' }
function world () {
var description = { slogan: '吃好每一頓飯' }
}
world()
}
hello()
上面的代碼所對應(yīng)的內(nèi)存堆??臻g如下圖所示:

棧中的垃圾回收比較簡單,當(dāng)一個函數(shù)執(zhí)行結(jié)束后,JavaScript 引擎會通過向下移動 ESP 來銷毀函數(shù)調(diào)用棧中所保存的執(zhí)行上下文,ESP 就是記錄當(dāng)前執(zhí)行狀態(tài)的指針。
堆中的垃圾數(shù)據(jù)
先來看兩個概念,能夠幫助我們更好的理解堆中的垃圾回收操作。
代際假說
堆中的垃圾回收策略都是建立在代際假說的基礎(chǔ)之上,代際假說有以下兩個特點(diǎn):
大部分對象在內(nèi)存中存在的時間很短,簡單來說,就是很多對象一經(jīng)分配內(nèi)存,很快就變得不可訪問。 不死的對象,會活得更久。
分代收集
在 Chrome 瀏覽器引擎 V8 中會把堆分為新生代和老生代兩個區(qū)域,如下圖所示:
顧名思義,生存時間短的對象放在新生區(qū)中,生存時間久的對象放在老生區(qū)中。
堆中的垃圾回收需要用到垃圾回收器,分為主垃圾回收器和副垃圾回收器。

副垃圾回收器
負(fù)責(zé)新生區(qū)的垃圾回收,新生區(qū)區(qū)域不大(為了執(zhí)行效率),回收頻繁。
新生區(qū)中使用了 Scavenge 算法,該算法會把新生區(qū)的空間劃分為兩個區(qū)域,一半是對象區(qū)域,一半是空閑區(qū)域。
副垃圾回收器的工作流程如下:
首先對對象區(qū)域中的垃圾進(jìn)行標(biāo)記。 標(biāo)記完成后,副垃圾回收器會將存活的對象復(fù)制到空閑區(qū)域中,為了避免產(chǎn)生內(nèi)存碎片,還需要進(jìn)行有序的排列,有序排列相當(dāng)于內(nèi)存整理。 完成復(fù)制后,將對象區(qū)域和空閑區(qū)域進(jìn)行翻轉(zhuǎn),就完成了垃圾回收的操作。
翻轉(zhuǎn)的這種操作可以讓對象區(qū)和空閑區(qū)無限重復(fù)的使用,不過由于新生區(qū)空間并不大,很容易會被存活的對象塞滿。所以 V8 引擎采用了對象晉升的策略,經(jīng)過兩次垃圾回收后依然還能存活的對象會被晉升到老生區(qū)中。
主垃圾回收器
負(fù)責(zé)老生區(qū)中的垃圾回收,老生區(qū)中對象占用空間大,對象存活時間長。
除了上文說到的新生區(qū)中晉升的對象,一些大的對象也會直接被分配到老生區(qū)。
主垃圾回收器是使用了標(biāo)記 - 清除(Mark-Sweep)的算法,工作流程如下:
首先是標(biāo)記階段,從一組根元素開始遞歸遍歷,能到達(dá)的元素就是活動對象,否則就是垃圾。 然后使用標(biāo)記 - 清除算法進(jìn)行垃圾回收,不過回收后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片。 于是又產(chǎn)生了另外一種算法 標(biāo)記 - 整理(Mark-Compact),整理時可以讓存活的對象都向一端移動,然后直接清除掉端邊界以外的內(nèi)存。
全停頓
垃圾回收操作會暫停 JavaScript 的運(yùn)行,回收完畢后才會恢復(fù)執(zhí)行,這種行為就是全停頓。
為了降低全停頓所帶來的卡頓,V8 引擎采用了增量標(biāo)記(Incremental Marking) 算法進(jìn)行優(yōu)化,將標(biāo)記過程分為一個個小任務(wù),這些小任務(wù)的執(zhí)行時間比較短,可以穿插在其他的 JavaScript 任務(wù)中間執(zhí)行,這樣就不會有明顯的卡頓了。
當(dāng)然,V8 所采用的優(yōu)化方案不只這一種,而是多種方案綜合使用的,除了增量回收還有并行回收、并發(fā)回收等。
并行回收:垃圾回收器會使用多個輔助線程來并行執(zhí)行垃圾回收 并發(fā)回收:回收線程在執(zhí)行 JavaScript 的過程中,輔助線程在后臺執(zhí)行垃圾回收
如果你了解 React 的 Concurrent 模式中時間切片的原理,它的實現(xiàn)思想是不是與增量標(biāo)記算法有異曲同工之妙呢。
04 核心網(wǎng)頁指標(biāo) Core Web Vitals
Google 大佬推出了 Core Web Vitals:目的是為了更好的簡化場景,幫助網(wǎng)站專注于最重要的指標(biāo)以提升用戶體驗。
在 2020 年主要關(guān)注三個方面:加載、交互性和視覺穩(wěn)定性,并包括以下指標(biāo):

衡量所有 Core Web Vitals 最簡單的方法就是使用 web-vitals 庫,使用起來就像調(diào)用單個函數(shù)一樣簡單。
import {getCLS, getFID, getLCP} from 'web-vitals';
getCLS(console.log);
getFID(console.log);
getLCP(console.log);
也可以使用 Chrome 插件 Web Vitals Chrome 來幫助我們測量這些指標(biāo)。
如果想要直接通過 Web API 來獲取這些指標(biāo)的話可以參考下面的獲取方法:
在JavaScript中測量LCP 在JavaScript中測量FID 在JavaScript中測量CLS
LCP Largest Contentful Paint 最大內(nèi)容繪制
LCP用于衡量標(biāo)準(zhǔn)報告視口內(nèi)可見的最大圖像或文本塊的渲染時間,為了提供良好的用戶體驗,網(wǎng)站應(yīng)努力在開始加載頁面的前2.5 秒內(nèi)進(jìn)行“最大內(nèi)容繪制”。

優(yōu)化LCP方案
FID First Input Delay 首次交互延遲
FID用于衡量從用戶第一次與頁面進(jìn)行交互到瀏覽器實際上能夠開始處理事件處理程序的時間。為了提供良好的用戶體驗,網(wǎng)站應(yīng)努力使首次輸入延遲小于 100 毫秒。

下圖中米色方塊代表主線程處于忙碌階段,如果此時用戶進(jìn)行輸入,則它必須等待任務(wù)完成時才能響應(yīng)輸入,等待的時間也就是此頁面上該用戶的 FID 值。

優(yōu)化FID方案
CLS Cumulative Layout Shift 累積布局偏移

CLS用于測量在頁面的整個生命周期中發(fā)生的每一個意外的布局移動,它代表所有單獨(dú)布局轉(zhuǎn)移分?jǐn)?shù)的總和。為了提供良好的用戶體驗,網(wǎng)站應(yīng)努力使CLS分?jǐn)?shù)小于0.1。
布局偏移分?jǐn)?shù)
瀏覽器將查看視口大小以及兩個渲染幀之間的視口中不穩(wěn)定元素的移動。
布局偏移分?jǐn)?shù)是該運(yùn)動的兩個指標(biāo)的乘積:影響分?jǐn)?shù)和距離分?jǐn)?shù)
layout shift score = impact fraction * distance fraction
影響分?jǐn)?shù)
前一幀和當(dāng)前幀的所有不穩(wěn)定元素的可見區(qū)域的并集(占視口總面積的一部分)是當(dāng)前幀的影響分?jǐn)?shù)。

在上圖中,有一個元素在一幀中占據(jù)了視口的一半。然后,在下一幀中,元素下移視口高度的 25%。紅色的虛線矩形表示兩個幀中元素的可見區(qū)域的并集,在這種情況下,其為總視口的 75%,因此其影響分?jǐn)?shù)為 0.75。
距離分?jǐn)?shù)
布局偏移分?jǐn)?shù)方程的另一部分測量不穩(wěn)定元素相對于視口移動的距離。距離分?jǐn)?shù)是任何不穩(wěn)定元素在框架中(水平或垂直)移動的最大距離除以視口的最大尺寸(寬度或高度,以較大者為準(zhǔn))。

在上圖中,最大視口尺寸是高度,不穩(wěn)定元素已經(jīng)移動了視口高度的 25%,所以距離分?jǐn)?shù)是 0.25。
所以,布局偏移分?jǐn)?shù):0.75 * 0.25 = 0.1875
優(yōu)化CLS方案
好了,本文到這里就結(jié)束了,文中參考的鏈接都整理到了下面,大家可以自行查閱。
站在巨人的肩膀上
圖解 Google V8 李兵 瀏覽器工作原理與實踐 李兵 Core Web Vitals https://web.dev/vitals/ web-vitals https://github.com/GoogleChrome/web-vitals/ LCP https://web.dev/lcp/ FID https://web.dev/fid/ CLS https://web.dev/cls/ 優(yōu)化FID方案 https://web.dev/optimize-fid/ 優(yōu)化LCP方案 https://web.dev/optimize-lcp/ 優(yōu)化CLS方案 https://web.dev/optimize-cls/
多一個點(diǎn)在看

多一條小魚干
