什麼是 CRP?
CRP
又稱關鍵渲染路徑,引用MDN
對它的解釋:css
❝關鍵渲染路徑是指瀏覽器經過把 HTML、CSS 和 JavaScript 轉化成屏幕上的像素的步驟順序。優化關鍵渲染路徑能夠提升渲染性能。關鍵渲染路徑包含了 Document Object Model (DOM),CSS Object Model (CSSOM),渲染樹和佈局。html
❞
優化關鍵渲染路徑能夠提高首屏渲染時間。理解和優化關鍵渲染路徑對於確保迴流和重繪能夠每秒 60 幀、確保高性能的用戶交互和避免無心義渲染相當重要。前端
如何結合CRP
進行性能優化?
我想對於性能優化,你們都不陌生,不管是平時的工做仍是面試,是一個老生常談的話題。vue
若是單純針對一些點去泛泛而談,我想是不太嚴謹的。react
今天咱們結合一道很是經典的面試題:從輸入URL到頁面展現,這中間發生了什麼?
來從其中的某些環節,來深刻談談前端性能優化 CRP
。web
從輸入 URL 到頁面展現,這中間發生了什麼?
這道題的經典程度想必不用我多說,這裏我用一張圖梳理了它的大體流程:這個過程能夠大體描述爲以下:面試
一、URI 解析算法
二、DNS 解析(DNS 服務器)瀏覽器
三、TCP 三次握手(創建客戶端和服務器端的鏈接通道)緩存
四、發送 HTTP 請求
五、服務器處理和響應
六、TCP 四次揮手(關閉客戶端和服務器端的鏈接)
七、瀏覽器解析和渲染
八、頁面加載完成
本文我會從瀏覽器渲染過程、緩存、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,拉你進技術交流羣一塊兒學習 🍻
本文分享自微信公衆號 - 全棧大佬的修煉之路(gh_7795af32a259)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。