前端性能優(yōu)化之關(guān)鍵路徑渲染優(yōu)化
瀏覽器加載流程
瀏覽器在渲染頁面時需要將 HTML 標記轉(zhuǎn)化成 DOM 對象
CSS 則會被轉(zhuǎn)化成 CSSOM 對象
DOM 和 CSSOM 是獨立的樹形結(jié)構(gòu),
當 DOM 樹和 CSSOM 樹都構(gòu)建完成的時候,他們就會合并在一起構(gòu)建 render tree,因為要在頁面上渲染不僅需要這個頁面的結(jié)構(gòu),也需要知道整個頁面的樣式,所以 render tree 是 DOM 樹和 CSSOM 樹的結(jié)合體,有了 render tree,瀏覽器才能知道把什么內(nèi)容按照什么樣式渲染在屏幕上。
瀏覽器從獲取 HTML 到最終在屏幕上顯示內(nèi)容需要完成以下步驟:
- 處理 HTML 標記并構(gòu)建 DOM 樹。
- 處理 CSS 標記并構(gòu)建 CSSOM 樹。
- 將 DOM 與 CSSOM 合并成一個 render tree。
- 根據(jù)渲染樹來布局,以計算每個節(jié)點的幾何信息。
- 將各個節(jié)點繪制到屏幕上。
經(jīng)過以上整個流程我們才能看見屏幕上出現(xiàn)渲染的內(nèi)容,優(yōu)化關(guān)鍵渲染路徑就是指最大限度縮短執(zhí)行上述第 1 步至第 5 步耗費的總時間,讓用戶最快的看到首次渲染的內(nèi)容。
另外,這是一個漸進的過程。為達到更好的用戶體驗,呈現(xiàn)引擎會力求盡快將內(nèi)容顯示在屏幕上。它不必等到整個 HTML 文檔解析完畢之后,就會開始構(gòu)建呈現(xiàn)樹和設(shè)置布局。在不斷接收和處理來自網(wǎng)絡(luò)的其余內(nèi)容的同時,呈現(xiàn)引擎會將部分內(nèi)容解析并顯示出來,因為 HTML 采用基于流的布局模型,這意味著大多數(shù)情況下只要一次遍歷就能計算出幾何信息。處于流中靠后位置元素通常不會影響靠前位置元素的幾何特征,因此布局可以按從左至右、從上至下的順序遍歷文檔。但是也有例外情況,比如 HTML 表格的計算就需要不止一次的遍歷。
阻塞渲染的因素
外部樣式表
從上面的整個流程我們已經(jīng)知道,瀏覽器的渲染需要 render tree, render tree 需要 CSSOM 樹才行,所以樣式表的加載是會阻塞頁面的渲染的,如果有一個外部的樣式表處于下載中,那么即使 HTML 已經(jīng)下載完畢,也會等待外部樣式表下載并解析完畢才會開始構(gòu)建 render tree。
腳本
腳本就更麻煩了,先明確一點, JS 引擎和 UI 的渲染引擎是互斥的,所以當腳本在執(zhí)行的時候瀏覽器要將控制權(quán)就給 JS 引擎,等到 JS 執(zhí)行完畢再還給 UI 引擎,不論這個腳本是以何種形式加載的,在執(zhí)行時均會阻塞 UI 的渲染。
接下來分別看不同形式加載的腳本對頁面渲染的阻塞情況:
內(nèi)聯(lián)腳本
內(nèi)聯(lián)的腳本隨著 HTML 一起下載,在開始執(zhí)行時已經(jīng)完成了 字節(jié) → 字符 → 令牌 → 節(jié)點 → 對象模型 的整個過程,所以不存在下載的時間(其實也不能這么說,下載的時間算在了 HTML 的下載時間中),執(zhí)行時是會阻塞關(guān)鍵渲染路徑的。
外部腳本
外部腳本的整個加載過程及執(zhí)行過程都是阻塞關(guān)鍵渲染路徑的。
帶 defer 和 async 的外部腳本
帶 defer/async 的腳本會與 HTML 并行下載,下載的過程不會阻塞 DOM 的構(gòu)建,但是執(zhí)行是會的,不同的是 defer 是在 DomContentLoaded 之前執(zhí)行,async 是加載完之后立刻執(zhí)行。
defer/async 的腳本在下載期間不會阻塞頁面解析不是一個技術(shù)原因而是一個選擇,因為內(nèi)聯(lián)腳本/外部腳本是要等待他們執(zhí)行,所以不得不等待他們下載。而頁面并不需要等待 defer/async 的腳本,所以他們的下載與頁面的解析是并行的。
動態(tài)生成的腳本
動態(tài)生成的腳本的下載過程不會阻塞頁面的解析,執(zhí)行會阻塞解析,有點 async 的感覺。
腳本與樣式表的依賴關(guān)系
腳本不僅能夠訪問 DOM 元素,還能訪問 DOM 的樣式,如果將要執(zhí)行腳本時瀏覽器尚未完成 CSSOM 的下載及構(gòu)建,瀏覽器將延遲腳本執(zhí)行和 DOM 構(gòu)建,直至其完成 CSSOM 的下載和構(gòu)建。
所以,CSSOM 的構(gòu)建會阻塞 HTML 的渲染,也會阻塞 JS 的執(zhí)行,JS 的下載與執(zhí)行(內(nèi)聯(lián)及外部樣式表)也會阻塞 HTML 的渲染。
優(yōu)化方法
為盡快完成首次渲染,我們需要最大限度減小以下三種可變因素:
- 關(guān)鍵資源的數(shù)量:可能阻止網(wǎng)頁首次渲染的資源。
- 關(guān)鍵路徑長度:獲取所有關(guān)鍵資源所需的往返次數(shù)或總時間。
- 關(guān)鍵字節(jié)的數(shù)量:實現(xiàn)網(wǎng)頁首次渲染所需的總字節(jié)數(shù),它是所有關(guān)鍵資源傳送文件大小的總和。我們包含單個 HTML 頁面的第一個示例包含一項關(guān)鍵資源(HTML 文檔);關(guān)鍵路徑長度也與 1 次網(wǎng)絡(luò)往返相等(假設(shè)文件較?。?,而總關(guān)鍵字節(jié)數(shù)正好是 HTML 文檔本身的傳送大小。
優(yōu)化關(guān)鍵渲染路徑的常規(guī)步驟如下:
- 對關(guān)鍵路徑進行分析和特性描述:資源數(shù)、字節(jié)數(shù)、長度。
- 最大限度減少關(guān)鍵資源的數(shù)量:刪除它們,延遲它們的下載,將它們標記為異步等。
- 優(yōu)化關(guān)鍵字節(jié)數(shù)以縮短下載時間(往返次數(shù))。
- 優(yōu)化其余關(guān)鍵資源的加載順序:您需要盡早下載所有關(guān)鍵資產(chǎn),以縮短關(guān)鍵路徑長度。
關(guān)鍵 CSS
上面已經(jīng)分析過了,樣式表會阻塞渲染,在加載完畢之前是不會顯示的,為了讓用戶以最快的速度看到頁面上的內(nèi)容,可以將頁面的某一部分的樣式抽離出來,單獨放在一個樣式表中或者內(nèi)聯(lián)在頁面中,這樣的樣式稱為關(guān)鍵樣式,這部分樣式會優(yōu)先它可以是頁面的骨架屏或者是用戶剛加載進頁面時看到的首屏的內(nèi)容。
預(yù)加載 —— preload & prefetch
使用 preload meta 來提升資源加載的優(yōu)先級。preload 的定義
preload is a declarative fetch, allowing you to force the browser to make a request for a resource without blocking the document’s onload event.
注意和 prefetch 的區(qū)別
<link rel=“prefetch”> is a directive that tells a browser to fetch a resource that will probably be needed for the next navigation. That mostly means that the resource will be fetched with extremely low priority
preload 會提升資源的優(yōu)先級因為它標明這個資源是本頁肯定會用到 —— 本頁優(yōu)先
prefetch 會降低這個資源的優(yōu)先級因為它標明這個資源是下一頁可能用到的 —— 為下一頁提前加載
preload 最大的作用就是將下載與執(zhí)行分離,并且將下載的優(yōu)先級提到了一個很高的地步,再由我們?nèi)タ刂瀑Y源執(zhí)行的位置。
加速樣式表下載
樣式表是阻塞頁面呈現(xiàn)的(注意是呈現(xiàn),不是解析),正常通過 link 加載的外部樣式表要等下載,構(gòu)建 CSSOM 樹才會讓頁面呈現(xiàn)完成,但是 preload 能夠讓樣式表的下載和呈現(xiàn)分離。
試想,當你在頁面的 head 中寫了如下的兩個樣式表:
第一個是關(guān)鍵 CSS,第二個不是關(guān)鍵 CSS,當頁面解析了這兩個 link 標簽后開始下載,但是即使 critical.css 下載解析完畢也不會呈現(xiàn)頁面,因為頁面還要下載和解析 non-critical.css。
這時候,就要將 non-critial.css 作為預(yù)加載,當樣式表作為被 preload 后,他就不會再阻塞頁面的呈現(xiàn),也就是所謂的異步下載,修改后的代碼如下:
如此一來,頁面在解析完 critical.css 之后就會呈現(xiàn)(暫不考慮腳本),而 non-critial 也在下載,但是并不阻塞頁面,指導(dǎo)它下載和解析完畢后才會應(yīng)用到頁面上。
現(xiàn)在并不是所有的瀏覽器都支持 preload,我們可以用 loadCSS 這個庫來做 polyfill,其實現(xiàn)的思路也是遍歷所有帶 preload 和 as 的標簽,然后修改標簽的 media 為不匹配任何條件并開始下載,在下載完畢后再還原該 link 原來的 media 標簽將它應(yīng)用。
加速腳本下載
preload 將腳本的加載及執(zhí)行分離,加了 preload 的 <link> 標簽的作用是將腳本提到高優(yōu)先級盡快完成下載,但并未執(zhí)行。
還需要在你想要他執(zhí)行的地方引入一個正常的 <script> 標簽執(zhí)行這個腳本
否則 chrome 大約會在 3s 后報一個 warning 來提醒你這個資源被浪費了完全沒有被使用到。
preload 的功能聽起來很像被 defer 的腳本,但是:
- defer 無法控制腳本執(zhí)行的時機,是在 DOMContentLoaded 執(zhí)行前觸發(fā)
- defer 會阻塞 DOMContentLoaded 事件
- defer 會阻塞 onload 事件,preload 不會阻塞 onload 事件
- defer 的腳本下載的優(yōu)先級是 low,preload 的腳本優(yōu)先級是 high
根據(jù)腳本在文檔中的位置不同和他們是否是 async,defer 和阻塞,它們會有不同的優(yōu)先級:
- 阻塞腳本在第一個圖片前發(fā)起請求的優(yōu)先級為:Medium(DevTools 中為 high)
- 阻塞腳本在第一個圖片后發(fā)情請求的優(yōu)先級為:Low(DevTools 中為 Medium)
- async/defer/動態(tài)插入的腳本(不論他們在文檔中的什么位置)的優(yōu)先級為:Lowest(DevTools 中為 Low)
我們以掘金的首頁為例:
可以看到 high 的全是寫在 HTML 中進行加載的靜態(tài)資源,Low 的都是 thunk 在 JS 中的腳本,是為其他頁面預(yù)加載的。
加速字體下載
自定義的字體在加載之前會處于 FOIT(Flash of Invisible Text)現(xiàn)象,具體的可以看 這篇文章,雖然我們可以使用類似 webFont 一類的庫來控制字體的閃現(xiàn)和添加鉤子函數(shù),但最佳解決方法還是讓字體的加載達到最快的速度。
使用 preload 也可以來加速字體的下載,在 head 中聲明 preload,比先下載樣式表再從中讀到 @font-face 的 src 再去加載要快得多。
但是要注意
preload 字體不帶 crossorigin 也將會二次獲?。?nbsp;確保你對 preload 的字體添加 crossorigin 屬性,否則他會被下載兩次,這個請求使用匿名的跨域模式。這個建議也適用于字體文件在相同域名下,也適用于其他域名的獲取(比如說默認的異步獲取)。
preload 如果不帶 crossorigin meta ,默認情況下 (即未指定 crossorigin 屬性時), CORS 根本不會使用,這樣 http 的 request header 中就不會有 origin,默認不去跨域,但是 @font-face 中去加載字體是默認跨域請求的,所以會造成兩次的 request header 不同,無法命中緩存,造成重復(fù)請求。
解決方法就是帶上 crossorigin,
空關(guān)鍵字和無效關(guān)鍵字都會被當做 anonymous。
其他資源
preload 不僅可以將這些在 head 中的資源加速,還可以提前加載一些隱藏在 CSS 和 JS 中的資源,比如剛才隱藏在 CSS 中的字體資源,或者 JS 中請求的資源。
preload 的標簽可以動態(tài)生成,這意味著在任何時候你都可以在頁面中提前加載但不執(zhí)行一個腳本,然后通過動態(tài)腳本來立刻執(zhí)行它。
媒體查詢
現(xiàn)在的頁面基本上都具有響應(yīng)式設(shè)計,即針對移動端或桌面端會采用 media 進行媒體查詢,有兩種包含媒體查詢的 CSS 代碼的方法:1. 將需要媒體查詢的代碼和基礎(chǔ)樣式代碼放在同一文件中,使用 @media 來使媒體查詢生效。 2. 將需要媒體查詢的代碼放在單獨的一個外部樣式表中,使用 media meta 對需要媒體查詢的 link 進行控制。
這兩種方法各有好處,如果需要媒體查詢的代碼量很小,那么和基礎(chǔ)樣式放在一起也沒有關(guān)系,可以節(jié)省一次 HTTP 請求。如果比較大的話,那么就會讓樣式表的體積增加,造成 FOUC 的時間變長,這時候更適合使用第二種。
另外請注意“阻塞渲染”僅是指瀏覽器是否需要暫停網(wǎng)頁的首次渲染,直至該資源準備就緒。無論哪一種情況,瀏覽器仍會下載 CSS 資源,但是不阻塞渲染的資源優(yōu)先級較低。
優(yōu)先級較低意味著瀏覽器在解析 HTML 時發(fā)現(xiàn)要下載這個樣式表,但并不一定會立刻開始下載,而是可能會將它滯后一段時間再下載(等級低沒人權(quán)),從 DevTools 上也可以看到 Highest 和 Lowest 的區(qū)別。
如果媒體查詢的樣式表符合當前的頁面,那么媒體查詢的樣式表也會阻塞關(guān)鍵路徑渲染(就好像他是個正常的一樣),同時,它的下載優(yōu)先級也會恢復(fù)到最高(恢復(fù)人權(quán))。
media 配合 preload 能做到響應(yīng)式加載資源,如下代碼,分別是兩副圖片適配移動端與 PC 端,如果不加 preload 的話,那么其中一幅就會以 Lowest 的等級延遲加載,但是如果我們是一個移動端優(yōu)先的網(wǎng)站,不希望用戶浪費流量及網(wǎng)速下載PC 端的大圖的話,就在每個 link 上加上 preload 即可,只有在打開網(wǎng)頁時符合 media 的資源會被加載,不符合 media 的資源始終不會被加載,即使后面將瀏覽器的寬度拉寬也不會加載。
如果用戶真的拉寬了屏幕,或者切換端設(shè)備,可以使用 Window.matchMedia,來進行 media 的匹配。
DNS 預(yù)解析 —— dns-prefetch
dns-prefetch 的使用方法更加簡單:
link 標簽的 rel 設(shè)定為 dns-prefetch,href 設(shè)定為需要預(yù)加載的主機域名即可。
在講 dns-prefetch 之前,先復(fù)習一遍 DNS 的作用及可以優(yōu)化的點才能了解 dns-prefetch 帶來的好處。
網(wǎng)絡(luò)通訊大部分是基于TCP/IP的,而TCP/IP是基于IP地址的,所以計算機在網(wǎng)絡(luò)上進行通訊時只能識別如“202.96.134.133”之類的IP地址,而不能認識域名。我們無法記住10個以上IP地址的網(wǎng)站,所以我們訪問網(wǎng)站時,更多的是在瀏覽器地址欄中輸入域名,就能看到所需要的頁面,這是因為有一個叫“DNS服務(wù)器”的計算機自動把我們的域名“翻譯”成了相應(yīng)的IP地址,然后調(diào)出IP地址所對應(yīng)的網(wǎng)頁。
一圖流表達如下,其中 3, 4, 5, 6, 7 都屬于 DNS 解析的過程,也是 dns-prefetch 發(fā)揮作用的地方。
dns-prefetch 主要用來在用戶點擊一個鏈接之前解析對應(yīng)的域名,這會自動去調(diào)用用戶瀏覽器的解析機制。瀏覽器會在用戶瀏覽網(wǎng)頁時多線程完成預(yù)加載,當用戶真正點擊的時候就節(jié)省了用戶等待域名解析的時間。
Chromium 的官方文檔中很詳細的介紹了 pre-fetch:
- Chromium 會根據(jù)頁面中超鏈接的 href 去尋找主機名自動去 prefetch
- 如果訪問的鏈接被重定向,那么瀏覽器可能無法自動識別出真正的主機進行 prefetch,此時需要我么手工預(yù)加載,也就是使用 prefetch 標簽來指定主機。(這也是決定是否使用 dns-prefetch 的判斷方法)
- 預(yù)加載不會對頁面渲染造成損害,因為 Chromium 有8個專門用來預(yù)加載的線程。
- dns-prefetch 帶來的網(wǎng)絡(luò)消耗是很小的
- Each request typically involves sending a single UDP packet that is under 100 bytes out, and getting back a response that is around 100 bytes
- 但是用最小的網(wǎng)絡(luò)開銷代價可以換來較好的用戶體驗。
- 默認情況下,Chromium 和 Firefox 出于安全考慮會關(guān)閉在 https 下的自動預(yù)加載,可以通過指定 meta http-equiv 來開啟自動預(yù)加載。
- <meta http-equiv="x-dns-prefetch-control" cnotallow="on">
- PS: 如果通過 meta 顯示的關(guān)閉了預(yù)加載,之后將無法再次開啟預(yù)加載。
拿知乎舉個例子,打開知乎,進入控制臺,搜索 dns-prefetch
發(fā)現(xiàn)知乎用了如下的 link,都是知乎的靜態(tài)資源服務(wù)器,因為在沒有緩存(假設(shè)沒有打開過知乎)時打開某個知乎頁面,如果該頁面有圖片,并且是從以上的域名獲取的話 dns-prefetch 就不會起作用。如果沒有圖片,那么上面的 dns-prefetch 就會解析域名,等到打開一個有圖的知乎頁面時 DNS 解析已經(jīng)完成了。
DNS 預(yù)解析 + TCP + TLS —— preconnect
提前加載整個頁面 —— prerender
以上兩者詳見:
- [譯] 資源提示 —— 什么是 Preload,Prefetch 和 Preconnect?
- [譯] Prefetch & preconnect-dns 的優(yōu)先級 性能優(yōu)化?
文章出自:??前端餐廳??,如有轉(zhuǎn)載本文請聯(lián)系前端餐廳今日頭條號。
github:??https://github.com/zuopf769??