使用 Preload&Prefetch 優化前端頁面的資源加載


對於前端頁面來說,靜態資源的加載對頁面性能起着至關重要的作用。本文將介紹瀏覽器提供的兩個資源指令-preload/prefetch,它們能夠輔助瀏覽器優化資源加載的順序和時機,提升頁面性能。

一、從一個實例開始

如上圖所示,我們開發了一個簡單的收銀台,支付過程中可以展開優惠券列表選擇相應的券。從動圖可以看到,列表第一次展開時,優惠券背景有一個逐漸顯示的過程,體驗上不是很好。

問題的原因也很明顯,由於背景使用了視覺特意設計的圖片,優惠券列表展開時需要去加載圖片,背景漸顯的過程實際上就是圖片加載的過程;當網速慢的時候,這個問題會更加明顯。那么,怎樣解決這個問題呢?

仔細分析一下,我們會發現問題的原因在於背景圖的加載時機太晚。

如果能在優惠券列表渲染前加載好背景圖,這個問題就不會出現。從這個思路出發,我們可能想到以下兩個方案:

  1. 使用內聯圖片,也就是將圖片轉換為base64編碼的data-url。這種方式,其實是將圖片的信息集成到css文件中,避免了圖片資源的單獨加載。但圖片內聯會增加css文件的大小,增加首屏渲染的時間。
  2. 使用js代碼對圖片進行預加載
preloadImage() {
    const imgList = [
        require('@/assets/imgs/error.png'),
        require('@/assets/imgs/ticket_bg.png')
    ];
    for (let i = 0; i < imgList.length; i++) {
        const newIMG = new Image();
        newIMG.src = imgList[i];
    }
}

這種方案主要是利用瀏覽器的緩存機制,由js代碼在特定時機提前加載相應圖片,優惠券列表渲染時就可以直接從緩存獲取。不過,這種方案增加了額外的代碼,需要自己控制好加載時機,並且將圖片的url硬編碼在了邏輯中。 

可以看出,以上兩種方案能夠解決我們的問題,但都存在一些缺點。

那么,有沒有更好的解決方案呢?答案是prefetch-一種由瀏覽器原生提供的預加載方案。

二、什么是prefetch?

prefetch(鏈接預取)是一種瀏覽器機制,其利用瀏覽器空閑時間來下載或預取用戶在不久的將來可能訪問的文檔。網頁向瀏覽器提供一組預取提示,並在瀏覽器完成當前頁面的加載后開始靜默地拉取指定的文檔並將其存儲在緩存中。當用戶訪問其中一個預取文檔時,便可以快速的從瀏覽器緩存中得到。--MDN

具體來說,瀏覽器通過<link rel="prefetch" href="/library.js">標簽來實現預加載。

其中rel="prefetch"被稱為Resource-Hints(資源提示),也就是輔助瀏覽器進行資源優化的指令。

類似的指令還有rel="preload",我們會在后文提及。

<head>
    ...
    <link rel="prefetch" href="static/img/ticket_bg.a5bb7c33.png">
    ...
</head>

查看現在優惠券列表的加載效果。

果然,成功達成了我們期望的效果。那么瀏覽器是如何做的呢?我們打開Chrome的Network面板一探究竟:

可以看到,在首屏的請求列表中已經出現了優惠券背景圖ticket_bg.png的加載請求,請求本身看起來和普通請求沒什么不同;展開優惠券列表后,network中增加了一次新的ticket_bg.png訪問請求,我們很快發現,這個請求的status雖然也是200,但有一個特殊的標記—prefetch cache,表明這次請求的資源來自prefetch緩存。這個表現驗證了上文中prefetch的定義,即瀏覽器在空閑時間預先加載資源,真正使用時直接從瀏覽器緩存中快速獲取。

三、Preload

從上面的案例,我們體會到了瀏覽器預加載資源的強大能力。實際上,預加載是一個廣義的概念,prefetch只是具體實現方式之一,本節我們介紹下另外一種預加載方式preload。上文我們提到,preload與prefetch同屬於瀏覽器的Resource-Hints,用於輔助瀏覽器進行資源優化。為了對兩者進行區分,prefetch通常翻譯為預提取,preload則翻譯為預加載。

元素的rel屬性的屬性值preload能夠讓你在你的HTML頁面中元素內部書寫一些聲明式的資源獲取請求,可以指明哪些資源是在頁面加載完成后即刻需要的。對於這種即刻需要的資源,你可能希望在頁面加載的生命周期的早期階段就開始獲取,在瀏覽器的主渲染機制介入前就進行預加載。這一機制使得資源可以更早的得到加載並可用,且更不易阻塞頁面的初步渲染,進而提升性能。

簡單來說,就是通過<link rel="preload" href="xxx" as="xx">標簽顯式聲明一個高優先級資源,強制瀏覽器提前請求資源,同時不阻塞文檔正常onload。我們同樣用一個實際案例進行詳細介紹。

上圖是我們開發的另外一個收銀台,出於本地化的考慮,設計上使用了自定義字體。開發完成后我們發現,頁面首次加載時文字會出現短暫的字體樣式閃動(FOUT,Flash of Unstyled Text),在網絡情況較差時比較明顯(如動圖所示)。究其原因,是字體文件由css引入,在css解析后才會進行加載,加載完成之前瀏覽器只能使用降級字體。也就是說,字體文件加載的時機太遲,需要告訴瀏覽器提前進行加載,這恰恰是preload的用武之地。

我們在入口html文件head加入preload標簽:

<head>
    ...
    <link rel="preload" as="font" href="<%= require('/assets/fonts/AvenirNextLTPro-Demi.otf') %>" crossorigin>
    <link rel="preload" as="font" href="<%= require('/assets/fonts/AvenirNextLTPro-Regular.otf') %>" crossorigin>
    ...
</head>

再次查看頁面首次加載的效果:

字體樣式閃動的現象沒有了!我們對比下使用preload前后的network面板。

使用前:

使用后:

可以發現字體文件的加載時機明顯提前了,在瀏覽器接收到html后很快就進行了加載。

注意:preload link必須設置as屬性來聲明資源的類型(font/image/style/script等),否則瀏覽器可能無法正確加載資源。

四、Preload 和 Prefetch 的具體實踐

1、preload-webpack-plugin

前文中我們舉的兩個例子,都是在入口html手動添加相關代碼:

<head>
    ...
    <link rel="prefetch" href="static/img/ticket_bg.a5bb7c33.png">
    ...
</head>
點擊並拖拽以移動
<head>
    ...
    <link rel="preload" as="font" href="<%= require('/assets/fonts/AvenirNextLTPro-Demi.otf') %>" crossorigin>
    <link rel="preload" as="font" href="<%= require('/assets/fonts/AvenirNextLTPro-Regular.otf') %>" crossorigin>
    ...
</head>

這顯然不夠方便,而且將資源路徑硬編碼在了頁面中(實際上,ticket_bg.a5bb7c33.png后綴中的hash是構建過程自動生成的,所以硬編碼的方式很多場景下本身就行不通)。webpack插件preload-webpack-plugin可以幫助我們將該過程自動化,結合htmlWebpackPlugin在構建過程中插入link標簽。

const PreloadWebpackPlugin = require('preload-webpack-plugin');
...
plugins: [
  new PreloadWebpackPlugin({
    rel: 'preload',
    as(entry) {  //資源類型
      if (/\.css$/.test(entry)) return 'style';
      if (/\.woff$/.test(entry)) return 'font';
      if (/\.png$/.test(entry)) return 'image';
      return 'script';
    },
    include: 'asyncChunks', // preload模塊范圍,還可取值'initial'|'allChunks'|'allAssets',
    fileBlacklist: [/\.svg/] // 資源黑名單
    fileWhitelist: [/\.script/] // 資源白名單
  })
]

PreloadWebpackPlugin配置總體上比較簡單,需要注意的是include屬性。該屬性默認取值'asyncChunks',表示僅預加載異步js模塊;如果需要預加載圖片、字體等資源,則需要將其設置為'allAssets',表示處理所有類型的資源。

但一般情況下我們不希望把預加載范圍擴得太大,所以需要通過fileBlacklist或fileWhitelist進行控制。

對於異步加載的模塊,還可以通過webpack內置的/_ webpackPreload: true _/標記進行更細粒度的控制。

以下面的代碼為例,webpack會生成<link rel="preload" href="chunk-xxx.js" as="script">標簽添加到html頁面頭部。

import(/* webpackPreload: true */ 'AsyncModule'); 

備注:prefetch的配置與preload類似,但無需對as屬性進行設置。

2、使用場景

從前文的介紹可知,preload的設計初衷是為了盡早加載首屏需要的關鍵資源,從而提升頁面渲染性能。

目前瀏覽器基本上都具備預測解析能力,可以提前解析入口html中外鏈的資源,因此入口腳本文件、樣式文件等不需要特意進行preload。

但是一些隱藏在CSS和JavaScript中的資源,如字體文件,本身是首屏關鍵資源,但當css文件解析之后才會被瀏覽器加載。這種場景適合使用preload進行聲明,盡早進行資源加載,避免頁面渲染延遲。 

與preload不同,prefetch聲明的是將來可能訪問的資源,因此適合對異步加載的模塊、可能跳轉到的其他路由頁面進行資源緩存;對於一些將來大概率會訪問的資源,如上文案例中優惠券列表的背景圖、常見的加載失敗icon等,也較為適用。

3、最佳實踐

基於上面對使用場景的分享,我們可以總結出一個比較通用的最佳實踐:

  • 大部分場景下無需特意使用preload
  • 類似字體文件這種隱藏在腳本、樣式中的首屏關鍵資源,建議使用preload
  • 異步加載的模塊(典型的如單頁系統中的非首頁)建議使用prefetch
  • 大概率即將被訪問到的資源可以使用prefetch提升性能和體驗

4、vue-cli3的默認配置

  • preload

默認情況下,一個Vue CLI應用會為所有初始化渲染需要的文件自動生成preload提示。這些提示會被@vue/preload-webpack-plugin注入,並且可以通過chainWebpack的config.plugin('preload')進行修改和刪除。

  • prefetch

默認情況下,一個Vue CLI應用會為所有作為async chunk生成的JavaScript文件(通過動態import()按需code splitting的產物)自動生成prefetch提示。這些提示會被@vue/preload-webpack-plugin注入,並且可以通過chainWebpack的config.plugin('prefetch')進行修改和刪除。

五、總結和踩坑

1、preload和prefetch的本質都是預加載,即先加載、后執行,加載與執行解耦。

2、preload和prefetch不會阻塞頁面的onload。

3、preload用來聲明當前頁面的關鍵資源,強制瀏覽器盡快加載;而prefetch用來聲明將來可能用到的資源,在瀏覽器空閑時進行加載。

4、不要濫用preload和prefetch,需要在合適的場景中使用。

5、preload的字體資源必須設置crossorigin屬性,否則會導致重復加載。 

原因是如果不指定crossorigin屬性(即使同源),瀏覽器會采用匿名模式的CORS去preload,導致兩次請求無法共用緩存。

6、關於preload和prefetch資源的緩存,在Google開發者的一篇文章中是這樣說明的:如果資源可以被緩存(比如說存在有效的cache-control和max-age),它被存儲在HTTP緩存(也就是disk cache)中,可以被現在或將來的任務使用;如果資源不能被緩存在HTTP緩存中,作為代替,它被放在內存緩存中直到被使用。 

然而我們在Chrome瀏覽器(版本號80)中進行測試,結果卻並非如此。將服務器的緩存策略設置為no-store,觀察下資源加載情況。

可以發現ticket_bg.png第二次加載並未從本地緩存獲取,仍然是從服務器加載。因此,如果要使用prefetch,相應的資源必須做好合理的緩存控制。

7、沒有合法https證書的站點無法使用prefetch,預提取的資源不會被緩存(實際使用過程中發現,原因未知)。

8、最后我們來看下preload和prefetch的瀏覽器兼容性。

可以看到,兩者的兼容性目前都還不是太好。好在不支持preload和prefetch的瀏覽器會自動忽略它,因此可以將它們作為一種漸進增強功能,優化我們頁面的資源加載,提升性能和用戶體驗。

作者: Sha Chaoheng


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM