圖解瀏覽器,解釋瀏覽器那些不為人知的小秘密
美味值:??????????
口味:仔梅燒小排

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

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

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

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

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

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

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

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

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

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

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

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

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

在上圖中,最大視口尺寸是高度,不穩(wěn)定元素已經(jīng)移動(dòng)了視口高度的 25%,所以距離分?jǐn)?shù)是 0.25。
所以,布局偏移分?jǐn)?shù):0.75 * 0.25 = 0.1875
優(yōu)化CLS方案
好了,本文到這里就結(jié)束了,文中參考的鏈接都整理到了下面,大家可以自行查閱。
站在巨人的肩膀上
圖解 Google V8 李兵 瀏覽器工作原理與實(shí)踐 李兵 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/
推薦公眾號(hào)
往期精文
