Chrome架構:僅僅打開了1個頁面,為什么有4個進程?
線程 VS 進程
線程是不能單獨存在的,它是由進程來啟動和管理的。一個進程就是一個程序的運行實例。詳細解釋就是,啟動一個程序的時候,操作系統會為該程序創建一塊內存,用來存放代碼、運行中的數據和一個執行任務的主線程,我們把這樣的一個運行環境叫進程。
並發的關鍵是你有處理多個任務的能力,不一定要同時。並行的關鍵是你有同時處理多個任務的能力。
chrome打開一個標簽頁:
1 個瀏覽器(Browser)主進程、1 個 GPU 進程、1 個網絡(NetWork)進程、多個渲染進程和多個插件進程。
- 瀏覽器進程。主要負責界面顯示、用戶交互、子進程管理,同時提供存儲等功能。
- 渲染進程。核心任務是將 HTML、CSS 和 JavaScript 轉換為用戶可以與之交互的網頁,排版引擎 Blink 和 JavaScript 引擎 V8 都是運行在該進程中,默認情況下,Chrome 會為每個 Tab 標簽創建一個渲染進程。出於安全考慮,渲染進程都是運行在沙箱模式下。
- GPU 進程。其實,Chrome 剛開始發布的時候是沒有 GPU 進程的。而 GPU 的使用初衷是為了實現 3D CSS 的效果,只是隨后網頁、Chrome 的 UI 界面都選擇采用 GPU 來繪制,這使得 GPU 成為瀏覽器普遍的需求。最后,Chrome 在其多進程架構上也引入了 GPU 進程。
- 網絡進程。主要負責頁面的網絡資源加載,之前是作為一個模塊運行在瀏覽器進程里面的,直至最近才獨立出來,成為一個單獨的進程。
- 插件進程。主要是負責插件的運行,因插件易崩潰,所以需要通過插件進程來隔離,以保證插件進程崩潰不會對瀏覽器和頁面造成影響。
TCP協議:如何保證頁面文件能被完整送達瀏覽器?
在衡量 Web 頁面性能的時候有一個重要的指標叫“FP(First Paint)”,是指從頁面加載到首次開始繪制的時長。其中一個重要的因素是網絡加載速度。
互聯網,實際上是一套理念和協議組成的體系架構。其中,協議是一套眾所周知的規則和標准,如果各方都同意使用,那么它們之間的通信將變得毫無障礙。
TCP(Transmission Control Protocol,傳輸控制協議)是一種面向連接的、可靠的、基於字節流的傳輸層通信協議。
tcp對於udp:
- 對於數據包丟失的情況,TCP 提供重傳機制;
- TCP 引入了數據包排序機制,用來保證把亂序的數據包組合成一個完整的文件。
HTTP請求流程:為什么很多站點第二次打開速度會很快?
瀏覽器端發起 HTTP 請求流程
- 構建請求
- 查找緩存
覽器緩存是一種在本地保存資源副本,以供下次請求時直接使用的技術。
好處:
緩解服務器端壓力,提升性能(獲取資源的耗時更短了);
對於網站來說,緩存是實現快速資源加載的重要組成部分。 - 准備 IP 地址和端口
HTTP 協議作為應用層協議,用來封裝請求的文本信息,TCP/IP 作傳輸層協議,HTTP 的內容是通過 TCP 的傳輸數據階段來實現的
第一步瀏覽器會請求 DNS 返回域名對應的 IP
當然瀏覽器還提供了DNS 數據緩存服務,如果某個域名已經解析過了,那么瀏覽器會緩存解析的結果,以供下次查詢時直接使用,這樣也會減少一次網絡請求。 - 等待 TCP 隊列
Chrome 有個機制,同一個域名同時最多只能建立 6 個 TCP 連接 - 建立 TCP 連接
- 發送 HTTP 請求
- 請求行: 包含請求方法、請求 URI(Uniform Resource Identifier)和 HTTP 版本協議。
- 請求頭
- 請求體
服務器端處理 HTTP 請求流程
- 返回請求
- 響應行: 包含協議版本和狀態碼。
- 響應頭
- 響應體
- 斷開連接
通常情況下,一旦服務器向客戶端返回了請求數據,它就要關閉 TCP 連接。不過如果瀏覽器或者服務器在其頭信息中加入了:Connection:Keep-Alive那么 TCP 連接在發送后將仍然保持打開狀態,這樣瀏覽器就可以繼續通過同一個 TCP 連接發送請求。保持 TCP 連接可以省去下次請求時需要建立連接的時間,提升資源加載速度。 - 重定向
響應行返回的狀態碼是 301,狀態 301 就是告訴瀏覽器,我需要重定向到另外一個網址,而需要重定向的網址正是包含在響應頭的Location字段中,接下來,瀏覽器獲取Location字段中的地址,並使用該地址重新導航,這就是一個完整重定向的執行流程。
1. 為什么很多站點第二次打開速度會很快?
第一次加載頁面過程中,緩存了一些耗時的數據,DNS 緩存和頁面資源緩存這兩塊數據是會被瀏覽器緩存的,通過響應頭中的 Cache-Control 字段來設置是否緩存該資源,如果緩存過期使用If-None-Match:"4f80f-13c-3a1xb12a"發送MD5加密的值和后端數據進行對比
2.如何保持登錄狀態
set-cookie
導航流程:從輸入URL到頁面展示,這中間發生了什么?
用戶發出 URL 請求到頁面開始解析的這個過程,就叫做導航。
從輸入 URL 到頁面展示
- 用戶輸入
關鍵字是搜索內容,還是請求的 URL - URL 請求過程
首先,網絡進程會查找本地緩存是否緩存了該資源。如果有緩存資源,那么直接返回資源給瀏覽器進程;如果在緩存中沒有查找到資源,那么直接進入網絡請求流程。這請求前的第一步是要進行 DNS 解析,以獲取請求域名的服務器 IP 地址。如果請求協議是 HTTPS,那么還需要建立 TLS 連接。
接下來就是利用 IP 地址和服務器建立 TCP 連接。連接建立之后,瀏覽器端會構建請求行、請求頭等信息,並把和該域名相關的 Cookie 等數據附加到請求頭中,然后向服務器發送構建的請求信息。
(1)重定向
301或302 這時網絡進程會從響應頭的 Location 字段里面讀取重定向的地址
(2)響應數據類型處理
Content-Type是 HTTP 頭中一個非常重要的字段, 它告訴瀏覽器服務器返回的響應體數據是什么類型,然后瀏覽器會根據 Content-Type 的值來決定如何顯示響應體的內容。
下載類型,那么該請求會被提交給瀏覽器的下載管理器,同時該 URL 請求的導航流程就此結束。但如果是HTML,那么瀏覽器則會繼續進行導航流程。 - 准備渲染進程
Chrome 的默認策略是,每個標簽對應一個渲染進程。但如果從一個頁面打開了另一個新頁面,而新頁面和當前頁面屬於同一站點的話,那么新頁面會復用父頁面的渲染進程 - 提交文檔
“提交文檔”的消息是由瀏覽器進程發出的,渲染進程接收到“提交文檔”的消息后,會和網絡進程建立傳輸數據的“管道”。
等文檔數據傳輸完成之后,渲染進程會返回“確認提交”的消息給瀏覽器進程。
瀏覽器進程在收到“確認提交”的消息后,會更新瀏覽器界面狀態,包括了安全狀態、地址欄的 URL、前進后退的歷史狀態,並更新 Web 頁面。 - 渲染階段
渲染流程:HTML、CSS和JavaScript,是如何變成頁面的?
按照渲染的時間順序,流水線可分為如下幾個子階段:構建 DOM 樹、樣式計算、布局階段、分層、繪制、分塊、光柵化和合成。
構建 DOM 樹
瀏覽器無法直接理解和使用 HTML,所以需要將 HTML 轉換為瀏覽器能夠理解的結構——DOM 樹。
構建布局樹
布局樹的結構基本上就是復制 DOM 樹的結構,不同之處在於 DOM 樹中那些不需要顯示的元素會被過濾掉,如 display:none 屬性的元素、head 標簽、script 標簽等。復制好基本的布局樹結構之后,渲染引擎會為對應的 DOM 元素選擇對應的樣式信息,這個過程就是樣式計算。
樣式計算(Recalculate Style)
css來源
- 通過 link 引用的外部 CSS 文件
<style>標記內的 CSS- 元素的 style 屬性內嵌的 CSS
- 把 CSS 轉換為瀏覽器能夠理解的結構
當渲染引擎接收到 CSS 文本時,會執行一個轉換操作,將 CSS 文本轉換為瀏覽器可以理解的結構——styleSheets。 - 轉換樣式表中的屬性值,使其標准化
需要將所有值轉換為渲染引擎容易理解的、標准化的計算2em -> 23px - 計算出 DOM 樹中每個節點的具體樣式
繼承規則,層疊規則。CSS 繼承就是每個 DOM 節點都包含有父節點的樣式。層疊是 CSS 的一個基本特征,它是一個定義了如何合並來自多個源的屬性值的算法。它在 CSS 處於核心地位,CSS 的全稱“層疊樣式表”正是強調了這一點。
布局階段
那么接下來就需要計算出 DOM 樹中可見元素的幾何位置,我們把這個計算過程叫做布局。
- 創建布局樹:包含可見元素布局樹
- 布局計算
分層
復雜的 3D 變換、頁面滾動,或者使用 z-indexing 做 z 軸排序等,為了更加方便地實現這些效果,渲染引擎還需要為特定的節點生成專用的圖層,並生成一棵對應的圖層樹(LayerTree)。
並不是布局樹的每個節點都包含一個圖層,如果一個節點沒有對應的層,那么這個節點就從屬於父節點的圖層。
第一點,擁有層疊上下文屬性的元素會被提升為單獨的一層。postion、z-index、filter、opacity
第二點,需要剪裁(clip)的地方也會被創建為圖層。
圖層繪制
渲染引擎實現圖層的繪制與之類似,會把一個圖層的繪制拆分成很多小的繪制指令,然后再把這些指令按照順序組成一個待繪制列表。
柵格化(raster)操作
繪制列表只是用來記錄繪制順序和繪制指令的列表,而實際上繪制操作是由渲染引擎中的合成線程來完成的。
根據視口(viewport)的大小,合成線程會將圖層划分為圖塊(tile),合成線程會按照視口附近的圖塊來優先生成位圖,實際生成位圖的操作是由柵格化來執行的。所謂柵格化,是指將圖塊轉換為位圖。
柵格化過程都會使用 GPU 來加速生成,使用 GPU 生成位圖的過程叫快速柵格化,或者 GPU 柵格化,生成的位圖被保存在 GPU 內存中。
合成和顯示
瀏覽器進程里面有一個叫 viz 的組件,用來接收合成線程發過來的 DrawQuad 命令,然后根據 DrawQuad 命令,將其頁面內容繪制到內存中,最后再將內存顯示在屏幕上
性能優化
- 更新了元素的幾何屬性(重排):會觸發重新布局,解析之后的一系列子階段,重排需要更新完整的渲染流水線,所以開銷也是最大的。
- 更新元素的繪制屬性(重繪):相較於重排操作,重繪省去了布局和分層階段,所以執行效率會比重排操作要高一些。
- 直接合成階段:只執行合成操作,transform 來實現動畫效果,這可以避開重排和重繪階段,直接在非主線程上執行合成動畫操作。
V8工作原理
V8工作原理和V8如何執行JavaScript代碼請移步到了解V8
Chrome開發者工具:利用網絡面板做性能分析
Chrome 開發者工具

網絡面板

- 控制器

- 過濾器
網絡面板中的過濾器,主要就是起過濾功能。因為有時候一個頁面有太多內容在詳細列表區域中展示了,而你可能只想查看 JavaScript 文件或者 CSS 文件,這時候就可以通過過濾器模塊來篩選你想要的文件類型。 - 抓圖信息
抓圖信息區域,可以用來分析用戶等待頁面加載時間內所看到的內容,分析用戶實際的體驗情況。比如,如果頁面加載 1 秒多之后屏幕截圖還是白屏狀態,這時候就需要分析是網絡還是代碼的問題了。(勾選面板上的“Capture screenshots”即可啟用屏幕截圖。) - 時間線
時間線,主要用來展示 HTTP、HTTPS、WebSocket 加載的狀態和時間的一個關系,用於直觀感受頁面的加載過程。如果是多條豎線堆疊在一起,那說明這些資源被同時被加載。至於具體到每個文件的加載信息,還需要用到下面要講的詳細列表。 - 詳細列表
這個區域是最重要的,它詳細記錄了每個資源從發起請求到完成請求這中間所有過程的狀態,以及最終請求完成的數據信息。通過該列表,你就能很容易地去診斷一些網絡問題。
詳細列表是我們本篇文章介紹的重點,不過內容比較多,所以放到最后去專門介紹了。 - 下載信息概要
下載信息概要中,你要重點關注下 DOMContentLoaded 和 Load 兩個事件,以及這兩個事件的完成時間。
DOMContentLoaded,這個事件發生后,說明頁面已經構建好 DOM 了,這意味着構建 DOM 所需要的 HTML 文件、JavaScript 文件、CSS 文件都已經下載完成了。
Load,說明瀏覽器已經加載了所有的資源(圖像、樣式表等)。
通過下載信息概要面板,你可以查看觸發這兩個事件所花費的時間。
網絡面板中的詳細列表
- 列表的屬性
- 詳細信息
- 單個資源的時間線
- Queuing
- Stalled
- Proxy Negotiation
- Initial connection/SSL
- Request sent
- Waiting (TTFB)第一字節時間
- Content Download
優化時間線上耗時項
- 排隊(Queuing)時間過久
排隊時間過久,大概率是由瀏覽器為每個域名最多維護 6 個連接導致的。那么基於這個原因,你就可以讓 1 個站點下面的資源放在多個域名下面,比如放到 3 個域名下面,這樣就可以同時支持 18 個連接了,這種方案稱為域名分片技術。除了域名分片技術外,我個人還建議你把站點升級到 HTTP2,因為 HTTP2 已經沒有每個域名最多維護 6 個 TCP 連接的限制了。 - 第一字節時間(TTFB)時間過久
這可能的原因有如下:
- 服務器生成頁面數據的時間過久。對於動態網頁來說,服務器收到用戶打開一個頁面的請求時,首先要從數據庫中讀取該頁面需要的數據,然后把這些數據傳入到模板中,模板渲染后,再返回給用戶。服務器在處理這個數據的過程中,可能某個環節會出問題。
- 網絡的原因。比如使用了低帶寬的服務器,或者本來用的是電信的服務器,可聯通的網絡用戶要來訪問你的服務器,這樣也會拖慢網速。
- 發送請求頭時帶上了多余的用戶信息。比如一些不必要的 Cookie 信息,服務器接收到這些 Cookie 信息之后可能需要對每一項都做處理,這樣就加大了服務器的處理時長。
對於這三種問題,你要有針對性地出一些解決方案。面對第一種服務器的問題,你可以想辦法去提高服務器的處理速度,比如通過增加各種緩存的技術;針對第二種網絡問題,你可以使用 CDN 來緩存一些靜態文件;至於第三種,你在發送請求時就去盡可能地減少一些不必要的 Cookie 數據信息。
- Content Download 時間過久
如果單個請求的 Content Download 花費了大量時間,有可能是字節數太多的原因導致的。這時候你就需要減少文件大小,比如壓縮、去掉源碼中不必要的注釋等方法。
DOM樹:JavaScript是如何影響DOM樹構建的?
什么是 DOM
從網絡傳給渲染引擎的 HTML 文件字節流是無法直接被渲染引擎理解的,所以要將其轉化為渲染引擎能夠理解的內部結構,這個結構就是 DOM。DOM 提供了對 HTML 文檔結構化的表述。
DOM 樹如何生成
在渲染引擎內部,有一個叫HTML 解析器(HTMLParser)的模塊,它的職責就是負責將 HTML 字節流轉換為 DOM 結構。
HTML 解析器並不是等整個文檔加載完成之后再解析的,而是網絡進程加載了多少數據,HTML 解析器便解析多少數據。
網絡進程接收到響應頭之后,會根據響應頭中的 content-type 字段來判斷文件的類型,比如 content-type 的值是“text/html”,那么瀏覽器就會判斷這是一個 HTML 類型的文件,然后為該請求選擇或者創建一個渲染進程。渲染進程准備好之后,網絡進程和渲染進程之間會建立一個共享數據的管道,網絡進程接收到數據后就往這個管道里面放,而渲染進程則從管道的另外一端不斷地讀取數據,並同時將讀取的數據“喂”給 HTML 解析器。
第一個階段,通過分詞器將字節流轉換為 Token。 Token,分為 Tag Token 和文本 Token。Tag Token 又分 StartTag 和 EndTag。
至於后續的第二個和第三個階段是同步進行的,需要將 Token 解析為 DOM 節點,並將 DOM 節點添加到 DOM 樹中。
HTML 解析器維護了一個Token 棧結構,該 Token 棧主要用來計算節點之間的父子關系,在第一個階段中生成的 Token 會被按照順序壓到這個棧中。(這是一個一邊壓棧一邊生成DOM節點的過程 ,文本 Token 不需要壓棧)
JavaScript 是如何影響 DOM 生成的
解析到<script>標簽時,渲染引擎判斷這是一段腳本,此時 HTML 解析器就會暫停 DOM 的解析,因為接下來的 JavaScript 可能要修改當前已經生成的 DOM 結構。
如果有src屬性,會通過src下載腳本,JavaScript 文件的下載過程會阻塞 DOM 解析。
JavaScript 腳本依賴樣式表(.css文件,因為會操作CSSOM),渲染引擎在遇到 JavaScript 腳本時,不管該腳本是否操縱了 CSSOM,都會執行 CSS 文件下載,解析操作,再執行 JavaScript 腳本。
優化:
- 預解析操作:當渲染引擎收到字節流之后,會開啟一個預解析線程,用來分析 HTML 文件中包含的 JavaScript、CSS 等相關文件,解析到相關文件之后,預解析線程會提前下載這些文件。
- 使用 CDN 來加速 JavaScript 文件的加載
- 壓縮 JavaScript 文件的體積
- JavaScript 文件中沒有操作 DOM 相關代碼,就可以將該 JavaScript 腳本設置為異步加載,通過 async 或 defer 來標記代碼(async加載完成,會立即執行;defer在 DOMContentLoaded 事件之前執行。)
渲染流水線:CSS如何影響首次加載時的白屏時間?
渲染流水線視角下的 CSS

其中有兩個渲染進程空閑時間:
- 渲染進程或瀏覽器進程發起的請求被送到網絡進程中去執行。網絡進程接收到返回的 HTML 數據之后,將其發送給渲染進程,渲染進程會解析 HTML 數據並構建 DOM。
- DOM 構建結束之后、.css 文件還未下載完成的這段時間內,渲染流水線無事可做,因為下一步是合成布局樹,而合成布局樹需要 CSSOM 和 DOM,所以這里需要等待 CSS 加載結束並解析成 CSSOM。
那渲染流水線為什么需要 CSSOM 呢?
渲染引擎無法直接理解 CSS 文件內容,所以需要將其解析成渲染引擎能夠理解的結構 CSSOM。CSSOM 具有兩個作用,第一個是提供給 JavaScript 操作樣式表的能力,第二個是為布局樹的合成提供基礎的樣式信息。(document.styleSheets)
body中有JavaScript腳本(不在body底部)執行流程:

由於JavaScript腳本依賴與CSSOM,所以 CSS 在部分情況下也會阻塞 DOM 的生成。
如果JavaScript腳本有src屬性和css文件通過link href屬性的執行流程:

影響頁面展示的因素以及優化策略
主要說瀏覽器渲染進程階段:解析 HTML、下載 CSS、下載 JavaScript、生成 CSSOM、執行 JavaScript、生成布局樹、繪制頁面一系列操作。
通常情況下的瓶頸主要體現在下載 CSS 文件、下載 JavaScript 文件和執行 JavaScript。
- 通過內聯 JavaScript、內聯 CSS 來移除這兩種類型的文件下載,這樣獲取到 HTML 文件之后就可以直接開始渲染流程了。
- 但並不是所有的場合都適合內聯,那么還可以盡量減少文件大小,比如通過 webpack 等工具移除一些不必要的注釋,並壓縮 JavaScript 文件。
- 還可以將一些不需要在解析 HTML 階段使用的 JavaScript 標記上 sync 或者 defer。
- 對於大的 CSS 文件,可以通過媒體查詢屬性,將其拆分為多個不同用途的 CSS 文件,這樣只有在特定的場景下才會加載特定的 CSS 文件。
分層和合成機制:為什么CSS動畫比JavaScript高效?
三個詞來概括:分層、分塊和合成。
顯示器是怎么顯示圖像的
每個顯示器都有固定的刷新頻率,通常是 60HZ,也就是每秒更新 60 張圖片,更新的圖片都來自於顯卡中一個叫前緩沖區的地方,顯示器所做的任務很簡單,就是每秒固定讀取 60 次前緩沖區中的圖像,並將讀取的圖像顯示到顯示器上。
顯卡的職責就是合成新的圖像,並將圖像保存到后緩沖區中,一旦顯卡把合成的圖像寫到后緩沖區,系統就會讓后緩沖區和前緩沖區互換,這樣就能保證顯示器能讀取到最新顯卡合成的圖像。
如何生成一幀圖像
總結為三種方式:
- 重排:速度最慢,需要重新計算布局
- 重繪:直接從繪制開始(次之)
- 合成(優先)
分層和合成(渲染進程分層到合成階段)
將素材分解為多個圖層的操作就稱為分層,最后將這些圖層合並到一起的操作就稱為合成。所以,分層和合成通常是一起使用的。在 Chrome 的渲染流水線中,分層體現在生成布局樹之后,渲染引擎會根據布局樹的特點將其轉換為層樹(Layer Tree),層樹是渲染流水線后續流程的基礎結構。需要重點關注的是,合成操作是在合成線程上完成的,這也就意味着在執行合成操作時,是不會影響到主線程執行的。
分塊
分層是從宏觀上提升了渲染效率,那么分塊則是從微觀層面提升了渲染效率。合成線程會將每個圖層分割為大小固定的圖塊,然后優先繪制靠近視口的圖塊,這樣就可以大大加速頁面的顯示速度。即使只繪制那些優先級最高的圖塊,也要耗費不少的時間,因為涉及到一個很關鍵的因素——紋理上傳,這是因為從計算機內存上傳到 GPU 內存的操作會比較慢。策略:在首次合成圖塊的時候使用一個低分辨率的圖片。
如何利用分層技術優化代碼
使用will-change,渲染引擎 box 元素將要做幾何變換和透明度變換操作,這時候渲染引擎會將該元素單獨實現一幀,等這些變換發生時,渲染引擎會通過合成線程直接去處理變換,這些變換並沒有涉及到主線程,這樣就大大提升了渲染的效率。 JavaScript 來寫這些效果,會牽涉到整個渲染流水線,這也是 CSS 動畫比 JavaScript 動畫高效的原因。缺點:需要額外的內存。
.box {
will-change: transform, opacity;
}
頁面性能:如何系統地優化頁面?
加載階段

能阻塞網頁首次渲染的資源稱為關鍵資源如:css文件,JavaScript腳本。基於關鍵資源,我們可以繼續細化出來三個影響頁面首次渲染的核心因素。
- 關鍵資源個數。關鍵資源個數越多,首次頁面的加載時間就會越長。
- 關鍵資源大小:資源內容越小,資源下載時間越短。
- 請求關鍵資源需要多少個 RTT(Round Trip Time):TCP 協議傳輸一個文件時,由於 TCP 的特性,這個數據並不是一次傳輸到服務端的,而是需要拆分成一個個數據包來回多次進行傳輸的。RTT 就是這里的往返時延。它是網絡中一個重要的性能指標,表示從發送端發送數據開始,到發送端收到來自接收端的確認,總共經歷的時延。通常 1 個 HTTP 的數據包在 14KB 左右,所以 1 個 0.1M 的頁面就需要拆分成 8 個包來傳輸了,也就是說需要 8 個 RTT。(只計算最大的資源的RTT)
針對上述情況優化: - JavaScript 和 CSS 改成內聯的形式;JavaScript 代碼沒有 DOM 或者 CSSOM 的操作,則可以改成 sync 或者 defer 屬性; CSS,如果不是在構建頁面之前加載的,則可以添加媒體取消阻止顯現的標志。(關鍵資源變為非關鍵資源)
- 壓縮 CSS 和 JavaScript 資源,移除注釋內容
- 減少關鍵資源的個數和減少關鍵資源的大小搭配來實現;使用 CDN 來減少每次 RTT 時長。
在優化實際的頁面加載速度時,你可以先畫出優化之前關鍵資源的圖表,然后按照上面優化關鍵資源的原則去優化,優化完成之后再畫出優化之后的關鍵資源圖表。
交互階段(用戶交互)
交互優化其實就是提高渲染進程渲染幀的速度。
可以從三方面入手:重排、重繪、合成。
- 減少 JavaScript 腳本執行時間(不要霸占太久主線程)
- 一種是將一次執行的函數分解為多個任務,使得每次的執行時間不要過久。
- 另一種是采用 Web Workers。
- 避免強制同步布局
通過 DOM 接口執行添加元素或者刪除元素等操作后,是需要重新計算樣式和布局的,不過正常情況下這些操作都是在另外的任務中異步完成的,這樣做是為了避免當前的任務占用太長的主線程時間。
// 可以用 Performance 工具來記錄添加元素的過程
// 結論:執行 JavaScript 添加元素是在一個任務中執行的,重新計算樣式布局是在另外一個任務中執行,這就是正常情況下的布局操作。
<html>
<body>
<div id="mian_div">
<li id="time_li">time</li>
<li>geekbang</li>
</div>
<p id="demo"> 強制布局 demo</p>
<button onclick="foo()"> 添加新元素 </button>
<script>
function foo() {
let main_div = document.getElementById("mian_div")
let new_node = document.createElement("li")
let textnode = document.createTextNode("time.geekbang")
new_node.appendChild(textnode);
document.getElementById("mian_div").appendChild(new_node);
}
</script>
</body>
</html>
所謂強制同步布局,是指 JavaScript 強制將計算樣式和布局操作提前到當前的任務中。
// 上述代碼變為同步布局,再用 Performance 工具記錄
function foo() {
let main_div = document.getElementById("mian_div")
let new_node = document.createElement("li")
let textnode = document.createTextNode("time.geekbang")
new_node.appendChild(textnode);
document.getElementById("mian_div").appendChild(new_node);
// 由於要獲取到 offsetHeight,
// 但是此時的 offsetHeight 還是老的數據,
// 所以需要立即執行布局操作
console.log(main_div.offsetHeight)
}
將新的元素添加到 DOM 之后,我們又調用了main_div.offsetHeight來獲取新 main_div 的高度信息。如果要獲取到 main_div 的高度,就需要重新布局,所以這里在獲取到 main_div 的高度之前,JavaScript 還需要強制讓渲染引擎默認執行一次布局操作。我們把這個操作稱為強制同步布局。
為了避免強制同步布局,我們可以調整策略,在修改 DOM 之前查詢相關值。代碼如下所示:
function foo() {
let main_div = document.getElementById("mian_div")
// 為了避免強制同步布局,在修改 DOM 之前查詢相關值
console.log(main_div.offsetHeight)
let new_node = document.createElement("li")
let textnode = document.createTextNode("time.geekbang")
new_node.appendChild(textnode);
document.getElementById("mian_div").appendChild(new_node);
}
- 避免布局抖動
布局抖動,是指在一次 JavaScript 執行過程中,多次執行強制布局和抖動操作。
for 循環語句里面不斷讀取屬性值,每次讀取屬性值之前都要進行計算樣式和布局。
function foo() {
let time_li = document.getElementById("time_li")
for (let i = 0; i < 100; i++) {
let main_div = document.getElementById("mian_div")
let new_node = document.createElement("li")
let textnode = document.createTextNode("time.geekbang")
new_node.appendChild(textnode);
new_node.offsetHeight = time_li.offsetHeight;
document.getElementById("mian_div").appendChild(new_node);
}
}
在 foo 函數內部重復執行計算樣式和布局,這會大大影響當前函數的執行效率。這種情況的避免方式和強制同步布局一樣,都是盡量不要在修改 DOM 結構時再去查詢一些相關值。
4. 合理利用 CSS 合成動畫(提示:will-change屬性)
5. 避免頻繁的垃圾回收:盡可能優化儲存結構,盡可能避免小顆粒對象的產生。
虛擬DOM:虛擬DOM和實際的DOM有何不同?
DOM 的缺陷
引發重排形象地理解就是“牽一發而動全身”,還有可能引發強制同步布局和布局抖動的問題,這些操作都會大大降低渲染效率。
什么是虛擬 DOM

- 創建階段。將多次的DOM操作組合在一起,創建出來虛擬 DOM(真實的 DOM 樹的結構)由虛擬 DOM 樹創建出真實 DOM 樹,真實的 DOM 樹生成完后,再觸發渲染流水線往屏幕輸出頁面。
- 更新階段。如果數據發生了改變(很多改變),那么就需要根據新的數據創建一個新的虛擬 DOM 樹;然后比較兩個樹,找出變化的地方,並把變化的地方一次性更新到真實的 DOM 樹上;最后渲染引擎更新渲染流水線,並生成新的頁面。(React Fiber reconciler 算法,就是在執行算法的過程中出讓主線程,這樣就解決了 Stack reconciler 函數占用時間過久的問題。)
1. 雙緩存
可以把虛擬 DOM 看成是 DOM 的一個 buffer,和圖形顯示一樣,它會在完成一次完整的操作之后,再把結果應用到 DOM 上,這樣就能減少一些不必要的更新,同時還能保證 DOM 的穩定輸出。
2. MVC 模式
核心思想就是將數據和視圖分離,根據不同的通信路徑和控制器不同的實現方式,基於 MVC 又能衍生出很多其他的模式,如 MVP、MVVM 等。
所以在分析基於 React 或者 Vue 這些前端框架時,我們需要先重點把握大的 MVC 骨架結構,然后再重點查看通信方式和控制器的具體實現方式,這樣我們就能從架構的視角來理解這些前端框架了。比如在分析 React 項目時,我們可以把 React 的部分看成是一個 MVC 中的視圖,在項目中結合 Redux 就可以構建一個 MVC 的模型結構,如下圖所示:

- 圖中的控制器是用來監控 DOM 的變化,一旦 DOM 發生變化,控制器便會通知模型,讓其更新數據;
- 模型數據更新好之后,控制器會通知視圖,告訴它模型的數據發生了變化;
- 視圖接收到更新消息之后,會根據模型所提供的數據來生成新的虛擬 DOM;
- 新的虛擬 DOM 生成好之后,就需要與之前的虛擬 DOM 進行比較,找出變化的節點;
- 比較出變化的節點之后,React 將變化的虛擬節點應用到 DOM 上,這樣就會觸發 DOM 節點的更新;
- DOM 節點的變化又會觸發后續一系列渲染流水線的變化,從而實現頁面的更新。
漸進式網頁應用(PWA):它究竟解決了Web應用的哪些問題?
PWA,全稱是 Progressive Web App,翻譯過來就是漸進式網頁應用。根據字面意思,它就是“漸進式 +Web 應用”。它是一套理念,漸進式增強 Web 的優勢,並通過技術手段漸進式縮短和本地應用或者小程序的距離。基於這套理念之下的技術都可以歸類到 PWA。
Web 應用 VS 本地應用
- Service Worker:解決離線存儲和消息推送的問題
在頁面和網絡之間增加一個攔截器,用來緩存和攔截請求。在沒有安裝 Service Worker 之前,WebApp 都是直接通過網絡模塊來請求資源的。安裝了 Service Worker 模塊之后,WebApp 請求資源時,會先通過 Service Worker,讓它判斷是返回 Service Worker 緩存的資源還是重新去網絡請求資源。一切的控制權都交由 Service Worker 來處理。 - manifest.json: 解決一級入口的問題
Service Worker 的設計思路
- 架構
“讓其運行在主線程之外”就是 Service Worker 來自 Web Worker 的一個核心思想。 Service Worker 需要在 Web Worker 的基礎之上加上儲存功能。由於 Service Worker 還需要會為多個頁面提供服務,所以還不能把 Service Worker 和單個頁面綁定起來。 - 消息推送
消息推送也是基於 Service Worker 來實現的。消息推送時,瀏覽器頁面也許並沒有啟動,這時就需要 Service Worker 來接收服務器推送的消息,並將消息通過一定方式展示給用戶。 - 安全
所以要使站點支持 Service Worker,首先必要的一步就是要將站點升級到 HTTPS。
WebComponent:像搭積木一樣構建Web應用
對內高內聚,對外低耦合。
阻礙前端組件化的因素
CSS 的全局屬性會阻礙組件化,DOM 也是阻礙組件化的一個因素,因為在頁面中只有一個 DOM,任何地方都可以直接讀取和修改 DOM。
WebComponent 組件化開發
提供了對局部視圖封裝能力,可以讓 DOM、CSSOM 和 JavaScript 運行在局部環境中,這樣就使得局部的 CSS 和 DOM 不會影響到全局。WebComponent 是一套技術的組合,具體涉及到了Custom elements(自定義元素)、Shadow DOM(影子 DOM)和HTML templates(HTML 模板)
- template 屬性來創建模板:
- DOM 樹中的 template 節點不會出現在布局樹中(不會渲染到頁面上);
- 可以被重復使用
- 創建一個類
- 查找模板內容;
- 創建影子 DOM;把影子 DOM 看成是一個作用域,其內部的樣式和元素是不會影響到全局的樣式和元素,而在全局環境下,要訪問影子 DOM 內部的樣式或者元素也是需要通過約定好的接口;在影子 DOM 定義的 JavaScript 函數可以被外部訪問,因為 JavaScript 語言本身已經可以很好地實現組件化。
- 再將模板添加到影子 DOM 上;
- 使用 customElements.define 來自定義元素。
- 像正常使用 HTML 元素一樣使用該元素
<geek-bang></geek-bang>
瀏覽器如何實現影子 DOM

HTTP/1:HTTP性能優化
超文本傳輸協議 HTTP/0.9

- 只有一個請求行,並沒有HTTP 請求頭和請求體。
- 服務器也沒有返回頭信息。
- 返回的文件內容是以 ASCII 字符流來傳輸。
被瀏覽器推動的 HTTP/1.0
支持多種類型的文件下載是 HTTP/1.0 的一個核心訴求

- 瀏覽器和服務器知道數據類型
- 壓縮方式傳輸
- 語言版本的頁面
- 文件的編碼類型
- 狀態碼
- Cache 機制
- 統計客戶端的基礎信息用戶代理字段
縫縫補補的 HTTP/1.1
- 改進持久連接:特點是在一個 TCP 連接上可以傳輸多個 HTTP 請求,只要瀏覽器或者服務器沒有明確斷開連接,那么該 TCP 連接會一直保持。持久連接在 HTTP/1.1 中是默認開啟,不想要采用持久連接,在 HTTP 請求頭中加上
Connection: close,瀏覽器中對於同一個域名,默認允許同時建立 6 個 TCP 持久連接。 - 不成熟的 HTTP 管線化
持久連接雖然能減少 TCP 的建立和斷開次數,但是它需要等待前面的請求返回之后,才能進行下一次請求。如果 TCP 通道中的某個請求因為某些原因沒有及時返回,那么就會阻塞后面的所有請求,這就是著名的隊頭阻塞的問題。HTTP/1.1 中試圖通過管線化的技術來解決隊頭阻塞的問題。HTTP/1.1 中的管線化是指將多個 HTTP 請求整批提交給服務器的技術,雖然可以整批發送請求,不過服務器依然需要根據請求順序來回復瀏覽器的請求。(目前沒有實現) - 提供虛擬主機的支持
一台物理主機上綁定多個虛擬主機,每個虛擬主機都有自己的單獨的域名,這些單獨的域名都公用同一個 IP 地址。(Host 字段,用來表示當前的域名地址) - 對動態生成的內容提供了完美支持
瀏覽器在傳輸數據之前並不知道最終的數據大小,這就導致了瀏覽器不知道何時會接收完所有的文件數據。Chunk transfer 機制來解決這個問題,服務器會將數據分割成若干個任意大小的數據塊,每個數據塊發送時會附上上個數據塊的長度,最后使用一個零長度的塊作為發送數據完成的標志。這樣就提供了對動態內容的支持。 - 客戶端 Cookie、安全機制
域名分片機制MDN
HTTP/2:如何提升網絡速度?
HTTP/1.1 的主要問題
對帶寬的利用率卻並不理想,帶寬是指每秒最大能發送或者接收的字節數。發送是上行帶寬,接受是下行寬帶。
- TCP 的慢啟動。
一旦一個 TCP 連接建立之后,就進入了發送數據狀態,剛開始 TCP 協議會采用一個非常慢的速度去發送數據,然后慢慢加快發送數據的速度,直到發送數據的速度達到一個理想狀態,我們把這個過程稱為慢啟動。 - 同時開啟了多條 TCP 連接,那么這些連接會競爭固定的帶寬。
- HTTP/1.1 隊頭阻塞的問題。(持久連接一個管道中同一時刻只能處理一個請求)
HTTP/2 的多路復用
總結為:繼續使用TCP建立連接,一個域名只使用一個 TCP 長連接和消除隊頭阻塞問題。

每個請求都有一個id標識,這是http/2的request特點,根據id可以不用按照順序發送request,瀏覽器和服務端可以通過id來進行組合大文件類型的請求(數據較大一次發送不完),服務端也可以通過id來優先決定先處理並返回關鍵資源的請求。
多路復用的實現
通過引入二進制分幀層,實現了 HTTP 的多路復用技術。

HTTP/2 添加了一個二進制分幀層,那我們就結合圖來分析下 HTTP/2 的請求和接收過程:
- 首先,瀏覽器准備好請求數據,包括了請求行、請求頭等信息,如果是 POST 方法,那么還要有請求體。
- 這些數據經過二進制分幀層處理之后,會被轉換為一個個帶有請求 ID 編號的幀,通過協議棧將這些幀發送給服務器。
- 服務器接收到所有幀之后,會將所有相同 ID 的幀合並為一條完整的請求信息。
- 然后服務器處理該條請求,並將處理的響應行、響應頭和響應體分別發送至二進制分幀層。
- 同樣,二進制分幀層會將這些響應數據轉換為一個個帶有請求 ID 編號的幀,經過協議棧發送給瀏覽器。
- 瀏覽器接收到響應幀之后,會根據 ID 編號將幀的數據提交給對應的請求。
HTTP/2 其他特性
- 可以設置請求的優先級
- 服務器推送:請求一個html文件可以相繼的發送html里鏈接的CSS和JavaScript文件;(CSS和JavaScript的請求可能是瀏覽器發起的)
- 頭部壓縮
http/2還是存在隊頭阻塞的問題:TCP傳輸過程中把一份數據分為多個數據包的。當其中一個數據包沒有按照順序返回,接收端會一直保持連接等待數據包返回,這時候就會阻塞后續請求。
HTTP/1.1 為了提升並行下載效率,瀏覽器為每個域名維護了 6 個 TCP 連接;而采用 HTTP/2 之后,瀏覽器只需要為每個域名維護 1 個 TCP 持久連接,同時還解決了 HTTP/1.1 隊頭阻塞的問題。
HTTP/3:甩掉TCP、TLS 的包袱,構建高效網絡
TCP 的隊頭阻塞
TCP 最初就是為了單連接而設計,TCP 連接看成是兩台計算機之前的一個虛擬管道,計算機的一端將要傳輸的數據按照順序放入管道,最終數據會以相同的順序出現在管道的另外一頭。


在 TCP 傳輸過程中,由於單個數據包的丟失而造成的阻塞稱為 TCP 上的隊頭阻塞。(http/2隊頭阻塞是發生在一個tcp連接管道,同一個域名中)

HTTP/2 多個請求是跑在一個 TCP 管道中的,其中任意一路數據流出現了丟包的情況,就會阻塞該 TCP 連接中的所有請求。這不同於 HTTP/1.1,使用 HTTP/1.1 時,瀏覽器為每個域名開啟了 6 個 TCP 連接,如果其中的 1 個 TCP 連接發生了隊頭阻塞,那么其他的 5 個連接依然可以繼續傳輸數據。
所以隨着丟包率的增加,HTTP/2 的傳輸效率也會越來越差。有測試數據表明,當系統達到了 2% 的丟包率時,HTTP/1.1 的傳輸效率反而比 HTTP/2 表現得更好。
TCP 建立連接的延時
TCP 的握手過程也是影響傳輸效率的一個重要因素。把從瀏覽器發送一個數據包到服務器,再從服務器返回數據包到瀏覽器的整個往返時間稱為 RTT,RTT 是反映網絡性能的一個重要指標。

以https協議為例,計算延遲過程:
- 建立 TCP 連接三次握手來確認連接成功,需要在消耗完 1.5 個 RTT 之后才能進行數據傳輸。
- 進行 TLS 連接,需要進行第二次握手,TLS 有兩個版本——TLS1.2 和 TLS1.3,每個版本建立連接所花的時間不同,大致是需要 1~2 個 RTT。
TCP 協議僵化
TCP 協議存在隊頭阻塞和建立連接延遲等缺點可以改進嗎?非常困難
- 中間設備的僵化:這些設備包括了路由器、防火牆、NAT、交換機等,些軟件使用了大量的 TCP 特性,這些功能被設置之后就很少更新。
- 操作系統:TCP 協議都是通過操作系統內核來實現的,應用程序只能使用不能修改。
開發新的協議,也會出現TCP 協議僵化相同的困境,沒有設備支持
QUIC 協議
HTTP/3 選擇了一個折衷的方法——UDP 協議,基於 UDP 實現了類似於 TCP 的多路數據流、傳輸可靠性等功能,我們把這套功能稱為QUIC 協議。

HTTP/3 中的 QUIC 協議集合了以下幾點功能:
- 實現了類似 TCP 的流量控制、傳輸可靠性的功能。雖然 UDP 不提供可靠性的傳輸,但 QUIC 在 UDP 的基礎之上增加了一層來保證數據可靠性傳輸。它提供了數據包重傳、擁塞控制以及其他一些 TCP 中存在的特性。
- 集成了 TLS 加密功能。目前 QUIC 使用的是 TLS1.3,相較於早期版本 TLS1.3 有更多的優點,其中最重要的一點是減少了握手所花費的 RTT 個數。
- 實現了 HTTP/2 中的多路復用功能。和 TCP 不同,QUIC 實現了在同一物理連接上可以有多個獨立的邏輯數據流(如下圖)。實現了數據流的單獨傳輸,就解決了 TCP 中隊頭阻塞的問題。

- 實現了快速握手功能。可以實現使用 0-RTT 或者 1-RTT 來建立連接。
HTTP/3 的挑戰
- 服務器和瀏覽器端都沒有對 HTTP/3 提供比較完整的支持;
- 部署 HTTP/3,UDP 的優化遠遠沒有達到 TCP 的優化程度;
- 中間設備僵化的問題,中間設備 UDP 的優化程度遠遠低於 TCP ,據統計使用 QUIC 協議時,大約有 3%~7% 的丟包率。
同源策略:為什么XMLHttpRequest不能跨域請求資源?
瀏覽器安全可以分為三大塊——Web 頁面安全、瀏覽器網絡安全和瀏覽器系統安全。
什么是同源策略
如果兩個 URL 的協議、域名和端口都相同,我們就稱這兩個 URL 同源。瀏覽器默認兩個相同的源之間是可以相互訪問資源和操作 DOM 的。
具體來講,同源策略主要表現在 DOM、Web 數據和網絡這三個層面。
- DOM 層面。同源策略限制了來自不同源的 JavaScript 腳本對當前 DOM 對象讀和寫的操作。
- 數據層面。同源策略限制了不同源的站點讀取當前站點的 Cookie、IndexDB、LocalStorage 等數據。
- 網絡層面。同源策略限制了通過 XMLHttpRequest 等方式將站點的數據發送給不同源的站點
安全和便利性的權衡
瀏覽器出讓了同源策略的安全性
- 頁面中可以嵌入第三方資源:任意引用外部文件
但此行為也衍生出了XSS攻擊,在html中惡意植入JavaScript文件,惡意腳本讀取 Cookie 數據,並將其作為參數添加至惡意站點尾部,當用戶不小心打開該惡意頁面時,惡意服務器就能接收到當前用戶的 Cookie 信息。
瀏覽器中引入了內容安全策略,稱為 CSP。CSP 的核心思想是讓服務器決定瀏覽器能夠加載哪些資源,讓服務器決定瀏覽器是否能夠執行內聯 JavaScript 代碼。 - 跨域資源共享和跨文檔消息機制
- 跨域資源共享(CORS)
- 跨文檔消息機制,通過 window.postMessage 的 JavaScript 接口來和不同源的 DOM 進行通信
跨站腳本攻擊(XSS):為什么Cookie中有HttpOnly屬性?
什么是 XSS 攻擊
XSS 全稱是 Cross Site Scripting,為了與“CSS”區分開來,故簡稱 XSS,翻譯過來就是“跨站腳本”。XSS 攻擊是指黑客往 HTML 文件中或者 DOM 中注入惡意腳本,從而在用戶瀏覽頁面時利用注入的惡意腳本對用戶實施攻擊的一種手段。
- 竊取 Cookie 信息
- 監聽用戶行為
- 修改 DOM
- 在頁面內生成浮窗廣告
惡意腳本是怎么注入的
存儲型 XSS 攻擊、反射型 XSS 攻擊和基於 DOM 的 XSS 攻擊三種方式來注入惡意腳本。
- 存儲型 XSS 攻擊
- 首先黑客利用站點漏洞將一段惡意 JavaScript 代碼提交到網站的數據庫中;
- 然后用戶向網站請求包含了惡意 JavaScript 腳本的頁面;
- 當用戶瀏覽該頁面的時候,惡意腳本就會將用戶的 Cookie 信息等數據上傳到服務器。
- 反射型 XSS 攻擊
在一個反射型 XSS 攻擊過程中,惡意 JavaScript 腳本屬於用戶發送給網站請求中的一部分,隨后網站又把惡意 JavaScript 腳本返回給用戶。當惡意 JavaScript 腳本在用戶頁面中被執行時,黑客就可以利用該腳本做一些惡意操作。Web 服務器不會存儲反射型 XSS 攻擊的惡意腳本,這是和存儲型 XSS 攻擊不同的地方。 - 基於 DOM 的 XSS 攻擊
基於 DOM 的 XSS 攻擊是不牽涉到頁面 Web 服務器的,在 Web 資源傳輸過程或者在用戶使用頁面的過程中修改 Web 頁面的數據。
如何阻止 XSS 攻擊
存儲型 XSS 攻擊和反射型 XSS 攻擊是服務器漏洞,DOM 的 XSS 攻擊是前端漏洞。
- 服務器對輸入腳本進行過濾或轉碼
- 充分利用 CSP
- 限制加載其他域下的資源文件,這樣即使黑客插入了一個 JavaScript 文件,這個 JavaScript 文件也是無法被加載的;
- 禁止向第三方域提交數據,這樣用戶數據也不會外泄;
- 禁止執行內聯腳本和未授權的腳本;
- 還提供了上報機制,這樣可以幫助我們盡快發現有哪些 XSS 攻擊,以便盡快修復問題。
- 使用 HttpOnly 屬性
set-cookie:id=1;HttpOnly,set-cookie 屬性值最后使用了 HttpOnly 來標記該 Cookie,無法通過 JavaScript 來讀取這段 Cookie。
CSRF攻擊:陌生鏈接不要隨便點
什么是 CSRF 攻擊
CSRF 英文全稱是 Cross-site request forgery,所以又稱為“跨站請求偽造”,是指黑客引誘用戶打開黑客的網站,在黑客的網站中,利用用戶的登錄狀態發起的跨站請求。簡單來講,CSRF 攻擊就是黑客利用了用戶的登錄狀態,並通過第三方的站點來做一些壞事。
- 自動發起 Get 請求
<img src="https://time.geekbang.org/sendcoin?user=hacker&number=100">
- 自動發起 POST 請求
<h1> 黑客的站點:CSRF 攻擊演示 </h1>
<form id='hacker-form' action="https://time.geekbang.org/sendcoin" method=POST>
<input type="hidden" name="user" value="hacker" />
<input type="hidden" name="number" value="100" />
</form>
<script> document.getElementById('hacker-form').submit(); </script>
- 引誘用戶點擊鏈接
<a href="https://time.geekbang.org/sendcoin?user=hacker&number=100" taget="_blank">
點擊下載美女照片
</a>
和 XSS 不同的是,CSRF 攻擊不需要將惡意代碼注入用戶的頁面,僅僅是利用服務器的漏洞和用戶的登錄狀態來實施攻擊。
如何防止 CSRF 攻擊
發起 CSRF 攻擊的三個必要條件:
- 目標站點一定要有 CSRF 漏洞;
- 用戶要登錄過目標站點,並且在瀏覽器上保持有該站點的登錄狀態;
- 需要用戶打開一個第三方站點,可以是黑客的站點,也可以是一些論壇。
要讓服務器避免遭受到 CSRF 攻擊,通常有以下幾種途徑:
- 充分利用好 Cookie 的 SameSite 屬性
- Strict 最為嚴格。如果 SameSite 的值是 Strict,那么瀏覽器會完全禁止第三方 Cookie。簡言之,如果你從極客時間的頁面中訪問 InfoQ 的資源,而 InfoQ 的某些 Cookie 設置了 SameSite = Strict 的話,那么這些 Cookie 是不會被發送到 InfoQ 的服務器上的。只有你從 InfoQ 的站點去請求 InfoQ 的資源時,才會帶上這些 Cookie。
- Lax 相對寬松一點。在跨站點的情況下,從第三方站點的鏈接打開和從第三方站點提交 Get 方式的表單這兩種方式都會攜帶 Cookie。但如果在第三方站點中使用 Post 方法,或者通過 img、iframe 等標簽加載的 URL,這些場景都不會攜帶 Cookie。
- 而如果使用 None 的話,在任何情況下都會發送 Cookie 數據。
- 驗證請求的來源站點
在服務器端驗證請求來源的站點,Referer 字段,記錄了該 HTTP 請求的來源地址,該字段不是必須的。Origin字段,如XMLHttpRequest、Fecth 發起跨站請求或者通過 Post 方法發送請求時,都會帶上 Origin 屬性。優先判斷 Origin,再根據實際情況判斷 Referer 值。 - CSRF Token
- 第一步,在瀏覽器向服務器發起請求時,服務器生成一個 CSRF Token。
- 第二步,在瀏覽器端如果要發起轉賬的請求,那么需要帶上頁面中的 CSRF Token,然后服務器會驗證該 Token 是否合法。
安全沙箱:頁面和系統之間的隔離牆
安全視角下的多進程架構

安全沙箱
將渲染進程和操作系統隔離的這道牆就是我們要聊的安全沙箱。瀏覽器中的安全沙箱是利用操作系統提供的安全技術,讓渲染進程在執行過程中無法訪問或者修改操作系統中的數據,在渲染進程需要訪問系統資源的時候,需要通過瀏覽器內核來實現,然后將訪問的結果通過 IPC 轉發給渲染進程。
安全沙箱如何影響各個模塊功能
安全沙箱最小的保護單位是進程,並且能限制進程對操作系統資源的訪問和修改,這就意味着如果要讓安全沙箱應用在某個進程上,那么這個進程必須沒有讀寫操作系統的功能,比如讀寫本地文件、發起網絡請求、調用 GPU 接口等。
渲染進程和瀏覽器內核各自都有哪些職責,如下圖:

- 持久存儲
- 存儲 Cookie 數據的讀寫。通常瀏覽器內核會維護一個存放所有 Cookie 的 Cookie 數據庫,然后當渲染進程通過 JavaScript 來讀取 Cookie 時,渲染進程會通過 IPC 將讀取 Cookie 的信息發送給瀏覽器內核,瀏覽器內核讀取 Cookie 之后再將內容返回給渲染進程。
- 一些緩存文件的讀寫也是由瀏覽器內核實現的,比如網絡文件緩存的讀取。
- 網絡訪問
- 用戶交互
站點隔離(Site Isolation)
所謂站點隔離是指 Chrome 將同一站點(包含了相同根域名和相同協議的地址)中相互關聯的頁面放到同一個渲染進程中執行。包括在標簽也中使用 iframe ,站點隔離會將不同源的 iframe 分配到不同的渲染進程中(而不是以標簽頁區分的渲染進程,這樣iframe惡意第三方地址不會和正常站點公用一個渲染進程)。
HTTPS:讓數據傳輸更安全
在 HTTP 協議棧中引入安全層
HTTPS 安全層有兩個主要的職責:對發起 HTTP 請求的數據進行加密操作和對接收到 HTTP 的內容進行解密操作。
第一版:使用對稱加密
對稱加密是指加密和解密都使用的是相同的密鑰。

具體流程:
- 瀏覽器發送它所支持的加密套件列表和一個隨機數 client-random,這里的加密套件是指加密的方法,加密套件列表就是指瀏覽器能支持多少種加密方法列表。
- 服務器會從加密套件列表中選取一個加密套件,然后還會生成一個隨機數 service-random,並將 service-random 和加密套件列表返回給瀏覽器。
- 最后瀏覽器和服務器分別返回確認消息。
問題:
傳輸 client-random 和 service-random 的過程是明文的,黑客可以拿到協商的加密套件和雙方的隨機數,隨機數合成密鑰的算法是公開的,所以黑客拿到隨機數之后,也可以合成密鑰。
第二版:使用非對稱加密
非對稱加密算法有 A、B 兩把密鑰,如果你用 A 密鑰來加密,那么只能使用 B 密鑰來解密;反過來,如果你要 B 密鑰來加密,那么只能用 A 密鑰來解密。

公鑰在瀏覽器端,私鑰在服務器端,瀏覽器發送給服務端的數據需要用私鑰解密,服務端發給瀏覽器的數據需要公鑰解密,但在請求的開始,公鑰和數據會一同發給客戶端。
問題:
- 第一個是非對稱加密的效率太低。
- 第二個是無法保證服務器發送給瀏覽器的數據安全。(服務器的數據可以通過公鑰解密,但公鑰是公開的)
第三版:對稱加密和非對稱加密搭配使用
在傳輸數據階段依然使用對稱加密,但是對稱加密的密鑰我們采用非對稱加密來傳輸。

pre-master 是經過公鑰加密之后傳輸的,所以黑客無法獲取到 pre-master,這樣黑客就無法生成密鑰,也就保證了黑客無法破解傳輸過程中的數據了。
加密套件是指加密的方法,加密套件列表就是指瀏覽器能支持多少種加密方法列表,提供給服務器選擇。
第四版:添加數字證書
服務器要證明這個服務器就是自己,需要使用權威機構頒發的證書,這個權威機構稱為CA(Certificate Authority),頒發的證書就稱為數字證書(Digital Certificate)。
數字證書有兩個作用:一個是通過數字證書向瀏覽器證明服務器的身份,另一個是數字證書里面包含了服務器公鑰。

公鑰包含在數字證書中,並且需要驗證數字證書是哪個服務器
申請免費證書
中文:https://freessl.cn/
英文:https://www.freessl.com/
如何申請數字證書
- 准備一套私鑰和公鑰
- 向 CA 機構提交公鑰、公司、站點等信息並等待認證
- 審核通過,CA 會向極客時間簽發認證的數字證書,包含了公鑰、組織信息、CA 的信息、有效時間、證書序列號等,這些信息都是明文的,同時包含一個 CA 生成的簽名。
數字簽名的過程: CA 使用 Hash 函數來計算極客時間提交的明文信息,並得出信息摘要;然后 CA 再使用它的私鑰對信息摘要進行加密,加密后的密文就是 CA 頒給極客時間的數字簽名。
瀏覽器如何驗證數字證書
瀏覽器讀取證書中相關的明文信息,采用 CA 簽名時相同的 Hash 函數來計算並得到信息摘要 A;再利用對應 CA 的公鑰解密簽名數據,得到信息摘要 B;如果信息摘要 A 和信息摘要 B 一致,確認證書是合法的。
瀏覽上下文組:如何計算Chrome中渲染進程的個數?
標簽頁之間的連接
通過<a>標簽來和新標簽建立連接,通過 JavaScript 中的 window.open 方法來和新標簽頁建立連接,不論這兩個標簽頁是否屬於同一站點,他們之間都能通過 opener 來建立連接,所以他們之間是有聯系的,在 WhatWG 規范中,把這一類具有相互連接關系的標簽頁稱為瀏覽上下文組 ( browsing context group)。Chrome 瀏覽器會將瀏覽上下文組中屬於同一站點的標簽分配到同一個渲染進程中。
可以使用
<a>標簽中的 noopener 和 noreferrer 屬性,來控制新打開的標簽頁是否需要瀏覽上下文組。
如果有 iframe 標簽,並且地址不是同一站點,則會分配到不同渲染進程中
任務調度:有了setTimeOut,為什么還要使用rAF(requestAnimationFrame)?
單消息隊列的隊頭阻塞問題
在單消息隊列架構下,存在着低優先級任務會阻塞高優先級任務的情況
Chromium 是如何解決隊頭阻塞問題的?
- 第一次迭代:引入高優先級隊列
實現了三個不同優先級的消息隊列,然后可以使用任務調度器來統一調度這三個不同消息隊列中的任務,可以實現優先隊列任務優先執行。
問題:將用戶輸入的消息或者合成消息添加進多個不同優先級的隊列中,任務的相對執行順序就會被打亂,甚至有可能出現還未處理輸入事件,就合成了該事件要顯示的圖片。 - 第二次迭代:根據消息類型來實現消息隊列
可以為不同類型的任務創建不同優先級的消息隊列:- 輸入事件的消息隊列,用來存放輸入事件。
- 合成任務的消息隊列,用來存放合成事件。
- 默認消息隊列,用來保存如資源加載的事件和定時器回調等事件。
- 創建一個空閑消息隊列,用來存放 V8 的垃圾自動垃圾回收這一類實時性不高的事件。
問題:在頁面加載階段,如果依然要優先執行用戶輸入事件和合成事件,那么頁面的解析速度將會被拖慢。
3. 第三次迭代:動態調度策略

- 頁面加載階段:訴求是在最短的時間看到頁面,頁面解析,JavaScript 腳本執行等任務調整為優先級最高的隊列。
- 交互階段:
前緩沖區存放着顯示器要顯示的圖像(60HZ,1/60 秒讀取一次前緩沖區),瀏覽器會將新生成的圖片提交到顯卡的后緩沖區中,GPU 會將后緩沖區和前緩沖區互換位置,也就完成顯示器的圖像顯示。
當顯示器將一幀畫面繪制完成后,並在准備讀取下一幀之前,顯示器會發出一個垂直同步信號(vertical synchronization)給 GPU,簡稱 VSync。

當在執行用戶交互的任務時,將合成任務的優先級調整到最高。主線程處理完成 DOM,計算好布局和繪制,需要將信息提交給合成線程來合成最終圖片,主線程合成任務的優先級調整為最低,並將頁面解析、定時器等任務優先級提升。
如果當前合成操作執行的非常快,比如用時8毫秒, VSync 同步周期是 16.66(1/60)毫秒,那么合成結束到下個 VSync 周期內,就進入了一個空閑時間階段,那么就可以在這段空閑時間內執行一些不那么緊急的任務。
- 第四次迭代:任務餓死
在某個狀態下,一直有新的高優先級的任務加入到隊列中,就會導致其他低優先級的任務得不到執行。Chromium 給每個隊列設置了執行權重,如果連續執行了一定個數的高優先級的任務,那么中間會執行一次低優先級的任務,可以緩解這種情況。
加載階段性能:使用Audits來優化Web性能
性能檢測工具:Performance vs Audits
Performance 和 Audits,能夠准確統計頁面在加載階段和運行階段的一些核心數據,諸如任務執行記錄、首屏展示花費的時長等。Perfomance 能讓我們看到更多細節數據,但是更加復雜,Audits 就比較智能,但是隱藏了更多細節。
利用 Audits 生成 Web 性能報告
解讀性能報告
根據性能報告優化 Web 性能

- 首次繪制 (First Paint):如果 FP 時間過久,那么直接說明了一個問題,那就是頁面的 HTML 文件可能由於網絡原因導致加載時間過久
- 首次有效繪制 (First Meaningfull Paint):由於 FMP 計算復雜,所以現在不建議使用該指標了,另外由於 LCP 的計算規則簡單,所以推薦使用 LCP 指標,具體文章你可以參考這里。不過是 FMP 還是 LCP,優化它們的方式都是類似的,你可以結合上圖,如果 FMP 和 LCP 消耗時間過久,那么有可能是加載關鍵資源花的時間過久,也有可能是 JavaScript 執行過程中所花的時間過久,所以我們可以針對具體的情況來具體分析。
- 首屏時間 (Speed Index):這就是我們上面提到的 LCP,它表示填滿首屏頁面所消耗的時間,首屏時間的值越大,那么加載速度越慢,具體的優化方式同優化第二項 FMP 是一樣。
- 首次 CPU 空閑時間 (First CPU Idle):也稱為 First Interactive,它表示頁面達到最小化可交互的時間,也就是說並不需要等到頁面上的所有元素都可交互,只要可以對大部分用戶輸入做出響應即可。要縮短首次 CPU 空閑時長,我們就需要盡可能快地加載完關鍵資源,盡可能快地渲染出來首屏內容,因此優化方式和第二項 FMP 和第三項 LCP 是一樣的。
- 完全可交互時間 (Time to Interactive):簡稱 TTI,它表示頁面中所有元素都達到了可交互的時長。簡單理解就這時候頁面的內容已經完全顯示出來了,所有的 JavaScript 事件已經注冊完成,頁面能夠對用戶的交互做出快速響應,通常滿足響應速度在 50 毫秒以內。如果要解決 TTI 時間過久的問題,我們可以推遲執行一些和生成頁面無關的 JavaScript 工作。
- 最大估計輸入延時 (Max Potential First Input Delay):這個指標是估計你的 Web 頁面在加載最繁忙的階段, 窗口中響應用戶輸入所需的時間,為了改善該指標,我們可以使用 WebWorker 來執行一些計算,從而釋放主線程。另一個有用的措施是重構 CSS 選擇器,以確保它們執行較少的計算。。
最后
Chromium源碼: https://chromium.googlesource.com/chromium/src
Chromium源碼文檔:https://chromium.googlesource.com/chromium/src/+/master/docs/README.md
