什麼是 CRP?
CRP
又稱關鍵渲染路徑,引用MDN
對它的解釋:
關鍵渲染路徑是指瀏覽器通過把 HTML、CSS 和 JavaScript 轉化成螢幕上的畫素的步驟順序。優化關鍵渲染路徑可以提高渲染效能。關鍵渲染路徑包含了 Document Object Model (DOM),CSS Object Model (CSSOM),渲染樹和佈局。
優化關鍵渲染路徑可以提升首屏渲染時間。理解和優化關鍵渲染路徑對於確保迴流和重繪可以每秒 60 幀、確保高效能的使用者互動和避免無意義渲染至關重要。
如何結合CRP
進行效能優化?
我想對於效能優化,大家都不陌生,無論是平時的工作還是面試,是一個老生常談的話題。
如果單純針對一些點去泛泛而談,我想是不太嚴謹的。
今天我們結合一道非常經典的面試題:從輸入URL到頁面展示,這中間發生了什麼?
來從其中的某些環節,來深入談談前端效能優化 CRP
。
從輸入 URL 到頁面展示,這中間發生了什麼?
這道題的經典程度想必不用我多說,這裡我用一張圖梳理了它的大致流程:
這個過程可以大致描述為如下:
1、URI 解析
2、DNS 解析(DNS 伺服器)
3、TCP 三次握手(建立客戶端和伺服器端的連線通道)
4、傳送 HTTP 請求
5、伺服器處理和響應
6、TCP 四次揮手(關閉客戶端和伺服器端的連線)
7、瀏覽器解析和渲染
8、頁面載入完成
本文我會從瀏覽器渲染過程、快取、DNS 優化幾方面進行效能優化的說明。
瀏覽器渲染過程
構建 DOM 樹
構建DOM
樹的大致流程梳理為下圖:
我們以下面這段程式碼為例進行分析:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
<title>構建DOM樹</title>
</head>
<body>
<p>森林</p>
<div>之晨</div>
</body>
</html>
首先瀏覽器從磁碟或網路中讀取 HTML
原始位元組,並根據檔案的指定編碼將它們轉成字元。
然後通過分詞器將位元組流轉換為 Token
,在Token
(也就是令牌)生成的同時,另一個流程會同時消耗這些令牌並轉換成 HTML head
這些節點物件,起始和結束令牌表明了節點之間的關係。
當所有的令牌消耗完以後就轉換成了DOM
(文件物件模型)。
最終構建出的DOM
結構如下:
構建 CSSOM 樹
DOM
樹構建完成,接下來就是CSSOM
樹的構建了。
與HTML
的轉換類似,瀏覽器會去識別CSS
正確的令牌,然後將這些令牌轉化成CSS
節點。
子節點會繼承父節點的樣式規則,這裡對應的就是層疊規則和層疊樣式表。
構建DOM
樹的大致流程可梳理為下圖:
我們這裡採用上面的HTML
為例,假設它有如下 css:
body {
font-size: 16px;
}
p {
font-weight: bold;
}
div {
color: orange;
}
那麼最終構建出的CSSOM
樹如下:
有了 DOM
和 CSSOM
,接下來就可以合成佈局樹(Render Tree)了。
構建渲染樹
等 DOM
和 CSSOM
都構建好之後,渲染引擎就會構造佈局樹。佈局樹的結構基本上就是複製 DOM
樹的結構,不同之處在於 DOM
樹中那些不需要顯示的元素會被過濾掉,如 display:none
屬性的元素、head
標籤、script
標籤等。
複製好基本的佈局樹結構之後,渲染引擎會為對應的 DOM
元素選擇對應的樣式資訊,這個過程就是樣式計算。
樣式計算
樣式計算的目的是為了計算出 DOM
節點中每個元素的具體樣式,這個階段大體可分為三步來完成。
把 CSS 轉換為瀏覽器能夠理解的結構
和 HTML
檔案一樣,瀏覽器也是無法直接理解這些純文字的 CSS
樣式,所以當渲染引擎接收到 CSS
文字時,會執行一個轉換操作,將 CSS
文字轉換為瀏覽器可以理解的結構——styleSheets
。
轉換樣式表中的屬性值,使其標準化
現在我們已經把現有的 CSS 文字轉化為瀏覽器可以理解的結構了,那麼接下來就要對其進行屬性值的標準化操作。
什麼是屬性值標準化?我們來看這樣的一段CSS
:
body {
font-size: 2em;
}
div {
font-weight: bold;
}
div {
color: red;
}
可以看到上面的 CSS
文字中有很多屬性值,如 2em、bold、red,這些型別數值不容易被渲染引擎理解,所以需要將所有值轉換為渲染引擎容易理解的、標準化的計算值,這個過程就是屬性值標準化。
那標準化後的屬性值是什麼樣子的?
從圖中可以看到,2em
被解析成了 32px
,bold
被解析成了 700
,red
被解析成了 rgb(255,0,0)
……
計算出 DOM 樹中每個節點的具體樣式
現在樣式的屬性已被標準化了,接下來就需要計算 DOM
樹中每個節點的樣式屬性了,如何計算呢?
這其中涉及到兩點:CSS 的繼承規則
和層疊規則
。
這裡由於不是本文的重點,我簡單做下說明:
CSS
繼承就是每個DOM
節點都包含有父節點的樣式- 層疊是
CSS
的一個基本特徵,它是一個定義瞭如何合併來自多個源的屬性值的演算法。它在CSS
處於核心地位,CSS
的全稱“層疊樣式表”正是強調了這一點。
樣式計算完成之後,渲染引擎還需要計算佈局樹中每個元素對應的幾何位置,這個過程就是計算佈局。
計算佈局
現在,我們有 DOM
樹和 DOM
樹中元素的樣式,但這還不足以顯示頁面,因為我們還不知道 DOM
元素的幾何位置資訊。那麼接下來就需要計算出 DOM
樹中可見元素的幾何位置,我們把這個計算過程叫做佈局
。
繪製
通過樣式計算和計算佈局就完成了最終佈局樹的構建。再之後,就該進行後續的繪製操作了。
到這裡,瀏覽器的渲染過程就基本結束了,通過下面的一張圖來梳理下:
到這裡我們已經把瀏覽器解析和渲染的完整流程梳理完成了,那麼這其中有那些地方可以去做效能優化呢?
從瀏覽器的渲染過程中可以做的優化點
通常一個頁面有三個階段:載入階段、互動階段和關閉階段。
- 載入階段,是指從發出請求到渲染出完整頁面的過程,影響到這個階段的主要因素有網路和
JavaScript
指令碼。 - 互動階段,主要是從頁面載入完成到使用者互動的整合過程,影響到這個階段的主要因素是
JavaScript
指令碼。 - 關閉階段,主要是使用者發出關閉指令後頁面所做的一些清理操作。
這裡我們需要重點關注載入階段
和互動階段
,因為影響到我們體驗的因素主要都在這兩個階段,下面我們就來逐個詳細分析下。
載入階段
我們先來分析如何系統優化載入階段中的頁面,來看一個典型的渲染流水線,如下圖所示:
通過上面對瀏覽器渲染過程的分析我們知道JavaScript
、首次請求的 HTML
資原始檔、CSS
檔案是會阻塞首次渲染的,因為在構建 DOM
的過程中需要 HTML
和 JavaScript
檔案,在構造渲染樹的過程中需要用到 CSS
檔案。
這些能阻塞網頁首次渲染的資源稱為關鍵資源
。而基於關鍵資源,我們可以繼續細化出三個影響頁面首次渲染的核心因素:
關鍵資源個數
。關鍵資源個數越多,首次頁面的載入時間就會越長。關鍵資源大小
。通常情況下,所有關鍵資源的內容越小,其整個資源的下載時間也就越短,那麼阻塞渲染的時間也就越短。請求關鍵資源需要多少個RTT(Round Trip Time)
。RTT
是網路中一個重要的效能指標,表示從傳送端傳送資料開始,到傳送端收到來自接收端的確認,總共經歷的時延。
瞭解了影響載入過程中的幾個核心因素之後,接下來我們就可以系統性地考慮優化方案了。總的優化原則就是減少關鍵資源個數
,降低關鍵資源大小
,降低關鍵資源的 RTT 次數
:
- 如何減少關鍵資源的個數?一種方式是可以將
JavaScript
和CSS
改成內聯的形式,比如上圖的JavaScript
和CSS
,若都改成內聯模式,那麼關鍵資源的個數就由 3 個減少到了 1 個。另一種方式,如果JavaScript
程式碼沒有DOM
或者CSSOM
的操作,則可以改成sync
或者defer
屬性 - 如何減少關鍵資源的大小?可以壓縮
CSS
和JavaScript
資源,移除HTML
、CSS
、JavaScript
檔案中一些註釋內容 - 如何減少關鍵資源
RTT
的次數?可以通過減少關鍵資源的個數和減少關鍵資源的大小搭配來實現。除此之外,還可以使用CDN
來減少每次RTT
時長。
互動階段
接下來我們再來聊聊頁面載入完成之後的互動階段以及應該如何去優化。
先來看看互動階段的渲染流水線:
其實這塊大致有以下幾點可以優化:
避免DOM的迴流
。也就是儘量避免重排
和重繪
操作。減少 JavaScript 指令碼執行時間
。有時JavaScript
函式的一次執行時間可能有幾百毫秒,這就嚴重霸佔了主執行緒執行其他渲染任務的時間。針對這種情況我們可以採用以下兩種策略:- 一種是將一次執行的函式分解為多個任務,使得每次的執行時間不要過久。
- 另一種是採用
Web Workers
。
DOM操作相關的優化
。瀏覽器有渲染引擎
和JS引擎
,所以當用JS
操作DOM
時,這兩個引擎要通過介面互相“交流”,因此每一次操作DOM
(包括只是訪問DOM
的屬性),都要進行引擎之間解析的開銷,所以常說要減少 DOM 操作。總結下來有以下幾點:- 快取一些計算屬性,如
let left = el.offsetLeft
。 - 通過
DOM
的class
來集中改變樣式,而不是通過style
一條條的去修改。 - 分離讀寫操作。現代的瀏覽器都有渲染佇列的機制。
- 放棄傳統操作
DOM
的時代,基於vue/react
等採用virtual dom
的框架
- 快取一些計算屬性,如
合理利用 CSS 合成動畫
。合成動畫是直接在合成執行緒上執行的,這和在主執行緒上執行的佈局、繪製等操作不同,如果主執行緒被JavaScript
或者一些佈局任務佔用,CSS
動畫依然能繼續執行。所以要儘量利用好CSS
合成動畫,如果能讓CSS
處理動畫,就儘量交給CSS
來操作。CSS選擇器優化
。我們知道CSS引擎
查詢是從右向左匹配的。所以基於此有以下幾條優化方案:- 儘量不要使用萬用字元
- 少用標籤選擇器
- 儘量利用屬性繼承特性
CSS屬性優化
。瀏覽器繪製影像時,CSS
的計算也是耗費效能的,一些屬性需瀏覽器進行大量的計算,屬於昂貴的屬性(box-shadows
、border-radius
、transforms
、filters
、opcity
、:nth-child
等),這些屬性在日常開發中經常用到,所以並不是說不要用這些屬性,而是在開發中,如果有其它簡單可行的方案,那可以優先選擇沒有昂貴屬性的方案。避免頻繁的垃圾回收
。我們知道JavaScript
使用了自動垃圾回收機制,如果在一些函式中頻繁建立臨時物件,那麼垃圾回收器也會頻繁地去執行垃圾回收策略。這樣當垃圾回收操作發生時,就會佔用主執行緒,從而影響到其他任務的執行,嚴重的話還會讓使用者產生掉幀、不流暢的感覺。
快取
快取可以說是效能優化中簡單高效的一種優化方式了。一個優秀的快取策略可以縮短網頁請求資源的距離,減少延遲,並且由於快取檔案可以重複利用,還可以減少頻寬,降低網路負荷。下圖是瀏覽器快取的查詢流程圖:
瀏覽器快取相關的知識點還是很多的,這裡我有整理一張圖:
關於瀏覽器快取的詳細介紹說明,可以參考我之前的這篇文章,這裡就不贅述了。
DNS 相關優化
DNS
全稱Domain Name System
。它是網際網路的“通訊錄”,它記錄了域名與實際ip
地址的對映關係。每次我們訪問一個網站,都要通過各級的DNS
伺服器查詢到該網站的伺服器ip
,然後才能訪問到該伺服器。
DNS
相關的優化一般涉及到兩點:瀏覽器DNS
快取和DNS
預解析。
DNS
快取
一圖勝千言:
- 瀏覽器會先檢查瀏覽器快取(瀏覽器快取有大小和時間限制),時間過長可能導致
IP
地址變化,無法解析正確IP
地址,過短就會讓瀏覽器重複解析域名,一般為幾分鐘。 - 如果瀏覽器快取沒有對應域名,則會去作業系統快取中查詢。
- 如果還沒有找到,域名就會傳送到本地區的域名伺服器(一般由網際網路供應商提供,電信、聯通之類),一般在本地區的域名伺服器上都能找到了。
- 當然也可能本地域名伺服器也沒找到,那本地域名伺服器就開始遞迴查詢。
一般而言,瀏覽器解析DNS
需要20-120ms
,因此DNS
解析可優化之處幾乎沒有。但存在這樣一個場景,網站有很多圖片在不同域名下,那如果在登入頁就提前解析了之後可能會用到的域名,使解析結果快取過,這樣縮短了DNS
解析時間,提高網站整體上的訪問速度了,這就是DNS預解析
。
DNS
預解析
來看下 MDN 對於DNS預解析
的定義吧:
X-DNS-Prefetch-Control
頭控制著瀏覽器的DNS
預讀取功能。DNS
預讀取是一項使瀏覽器主動去執行域名解析的功能,其範圍包括文件的所有連結,無論是圖片的,CSS
的,還是JavaScript
等其他使用者能夠點選的URL
。
因為預讀取會在後臺執行,所以 DNS
很可能在連結對應的東西出現之前就已經解析完畢。這能夠減少使用者點選連結時的延遲。
我們這裡就簡單看一下如何去做DNS預解析
:
- 在頁面頭部加入,這樣瀏覽器對整個頁面進行預解析
<meta http-equiv="x-dns-prefetch-control" content="on" />
- 通過 link 標籤手動新增要解析的域名,比如:
<link rel="dns-prefetch" href="//img10.360buyimg.com" />
參考
李兵 「瀏覽器工作原理與實踐」
❤️ 愛心三連擊
1.如果覺得這篇文章還不錯,來個分享、點贊、在看三連吧,讓更多的人也看到~
2.關注公眾號前端森林,定期為你推送新鮮乾貨好文。
3.特殊階段,帶好口罩,做好個人防護。
4.新增微信fs1263215592,拉你進技術交流群一起學習 ?