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