Web前端資源預加載

2020-03-30     sandag

本文主要介紹前端性能優化中的資源預加載,不僅會介紹常規的一些預加載手段;還會介紹工程實踐中的應用。

涉及內容:

  • link相關(rel、as、media、defer、async);
  • 緩存(4種緩存、緩存策略、ServiceWork);
  • 優化網絡(HTTP/2 ServerPush、Preload/Prefetch、域名拆分);
  • 同步接口JSON數據內聯,加速首頁渲染;
  • 瀏覽器中各資源加載的優先級;
  • 實踐:webpack插件、quiklink.

前言

當我們需要某些網絡資源時,加載和執行往往耦合在一起,下載完立即執行,而加載過程是阻塞式的,延長了onload時間。因此如何在資源執行前預加載資源,減少等待網絡的開銷便是我們要探討的問題。

1. 常規做法

附一張不同資源瀏覽器優先級的圖示(來源):

1)async/defer: 無阻塞加載

defer:DOMContentLoaded事件觸發前執行;在現實當中,延遲腳本並不一定會按照順序執行,也不一定會在DOMContentLoaded事件觸發前執行,因此最好只包含一個延遲腳本;

async:加載完立即執行,無法控制執行時機和執行順序。適用於無依賴的外部獨立資源。

不足:僅限於腳本資源;執行時機不可控或存在執行順序問題,用於非關鍵資源。

2)使用ajax加載資源:可以實現預加載。

不足:優先級較低,無法對首屏資源提前加載。

3)Webkit瀏覽器預測解析:chrome的預加載掃描器html-preload-scanner通過掃描節點中的 "src" , "link"等屬性,找到外部連接資源後進行預加載,避免了資源加載的等待時間,同樣實現了提前加載以及加載和執行分離。

原始解析做法:

採用預解析(掃描)器:

不足:

- 僅限解析HTML中收集到的外鏈資源,對JS異步加載的資源無法提前收集。

- 未暴露類似於Preload的onload事件。

4)Server Push 圖片來源

資源推送:

Link: ; rel=preload; as=style;

資源僅預加載,不推送:

Link: ; rel=preload; as=style;nopush

目標:減少請求數量和提高頁面加載速度。

特點:多頁面共享push cache(動態數據json除外)

適用場景:如果不推送這個資源,瀏覽器就會請求這個資源。

需要注意:要確保沒有發起不必要的推送,浪費流量。可以使用preload標籤代替,或者在HTTP頭中加nopush屬性。

Tips:如果伺服器或者瀏覽器不支持 HTTP/2,那麼瀏覽器就會按照 preload 來處理這個頭信息,預加載指定的資源文件。

不足:

- Edge和Safari的支持不好,慎用;

- 如果瀏覽器不從push cache中獲取資源,推送反而不利於網頁性能;

- 只能推送不帶請求體的GET和HEAD請求;

- push cache中的資源只能使用一次;

- 不考慮客戶端的緩存,始終推送:

- 只對第一次訪問的用戶開啟伺服器推送;

- 保守起見,推送原本內聯的資源,這樣即使多推,也比內聯效果好點;

- 將資源文件的緩存狀態更新至客戶端的Cookie。

-僅能推送同源資源;

-push cache: 只在會話中存在,一旦會話結束就會被釋放;

- 即使push的是最新的資源,如果http緩存中max-age沒有過期,仍然使用http緩存中的資源。(資源依次查找緩存的順序:內存緩存、Service Worker緩存、Disk緩存、Push緩存)

- 無load/error事件

【問題】

不僅js渲染阻塞,有時需要等待js執行後獲取一些數據(JSON)後,才能真正渲染完成,如何解決?(參考圖片來源)

1)動態資源的提前推送,注意參數需固定,不帶隨機變量的;

有一丟丟像webpack異步模塊加載中__webpack_require__.e的實現,感興趣的童鞋看 這裡

2)服務端渲染(直出同構)

比如node層模板渲染時,將同步接口中拉取的數據同步數據先緩存起來,如掛載在window._data,使用時直接從全局變量上讀取。

好了,上面介紹了一些常用的預加載方法,下面主角登場~

preload和prefetch

概念

preload

聲明式的 fetch,可以強制瀏覽器請求資源,同時不阻塞文檔 onload 事件。使用場景:當前頁面使用(一般用於和首屏渲染無關的邏輯,比如數據打點等),儘早下載,優先級較高;

prefetch

首次渲染時不需要,之後可能需要。優先級較低,在瀏覽器空閒時才會下載。使用場景:比如當前頁可能跳轉的頁面,或者條件加載的資源。

特點

- preload的資源存儲在內存緩存中(沒有設置資源的緩存策略時);

- 下載但不執行;

- 異步加載,不影響當前頁面的渲染;

- 提前加載資源,在真正使用時,直接從從緩存中讀取;

- 使用場景

- 當分析當前頁面用戶高頻點擊的連結,分析提取跳轉頁上的資源,使用prefetch預加載。

- font字體文件的預加載由於字體文件必須等到CSSOM構建完成並且作用到頁面元素了才會開始加載,會導致頁面字體樣式閃動。而瀏覽器為了避免FOUT,會儘量等待字體加載完成後,再顯示應用了該字體的內容,會導致加載完成前顯示空白。

檢測prelaod和prefetch的支持情況

let { relList } = document.createElement('link');

如何使用

//link標籤

as屬性值

不同值表明資源類型,對應的優先級不同:style, script, image, media, document, font。(問題:官方說法:不帶 「as」屬性的 preload 的優先級將會等同於異步請求。測試後發現不寫as並沒有發請求。)

注意事項

- 造成二次下載

- 同一資源分別使用as='style'和as='script'預加載,會造成二次下載;

- prefetch和preload同時對同一資源使用,會造成二次下載;

- 實際是script腳本,但使用as='style'會造成二次下載;

- preload字體時不帶crossOrigin(默認指定anonymous匿名,不帶認證信息),同樣會造成二次下載preload字體時即使同域也需要帶crossOrigin,否則同樣會造成二次下載;Requests without credentials use a separate connection。

- 沒有使用preload資源,Chrome會在onload事件3s後做出警示,避免無效的優化,浪費流量。

-瀏覽器對同一域名有並行加載數限制,因此考慮域名拆分等優化。

實踐

下面是twitter頁面中應用:

preload的polyfill

背景知識

//若支持preload,異步下載完不會立即執行

polyfill實現思路

參見loadCSS,提供了css的preload的polyfill實現(只展示關鍵代碼):

// 1. 【支持preload的情況】

注意這裡實現的是對CSS的預加載。

適用於對於首頁無關的樣式:

由於preload能夠異步加載樣式,因此可以避免在加載首頁無關樣式時阻塞初始渲染。

對於首頁初始渲染中重要的樣式:

1)內聯 (注意,會將靜態資源的緩存策略與頁面的緩存策略捆綁)

2)HTTP/2的serverPush

「知道了preload和prefetch的用途,那如何結合項目實踐呢?由於webpack目前基本是項目必備,所以首先介紹結合webpack的使用;然後對quiklink進行簡單介紹。

結合webpack的實踐

1. 使用插件:PreloadWebpackPlugin

常用的配置如下:

new PreloadWebpackPlugin({

其中include的兩種使用:

1) 根據chunk類型進行處理:

include: 'asyncChunks' | 'initial' | 'allChunks'

asyncChunks:import()動態導入的模塊,可以使用prefetch方式異步加載模塊;

initial:初始化需要的模塊;

allChunks:處理所有模塊(asyncChunks & initial)。

2) 對已知命名的chunk,可以更精確的使用數組的方式配置需要使用的chunk

include: ['vendor', 'index']

注意事項

1)需要結合HtmlWebpackPlugin插件使用

2)必須放到HtmlWebpackPlugin後面,因為PreloadWebpackPlugin需要使用其提供的hook鉤子將構造的插入html中:

plugins: [

使用效果

對某個頁面中include的資源,最終會在對應頁面head中插入link標籤:

當真正使用時,由於已經下載到本地,直接讀取執行,性能得到較大的提升。

2. 結合import()

好處:拆分chunk,減少首屏js體積。

如果工程沒有使用HtmlWebpackPlugin,可以對動態導入的資源做如下處理:

import(/* webpackPrefetch: true */)

【版本限制】需webpack v4.6.0+ 才支持預取和預加載。本地測試後,發現prefetch可用,preload無效。

quiklink

旨在成為根據用戶viewport中的連結預取內容的簡易解決方案。

工作原理

通過獲取頁面中a標籤的href,試圖更快的加載接下來可能要訪問的頁面。

1) IntersectionObserver(交叉觀察器): 檢測當前視口的links

let target= document.getElementById('a');

【備註】常規的主要是通過getBoundingClientRect()獲取元素在視口中的詳細位置,來實現滾動加載以及吸附等功能。

2) requestIdleCallback:等到瀏覽器空閒時;

注意其和requestAnimationFrame的區別.

3) 檢查當前的網絡環境:

navigator.connection.effectiveType //4G、2G...;

4) 提供了多種方式預取連結

小巧的js庫,使用了如上4個特性,每一個都值得細細品味。有興趣的可以看下源碼: https://github.com/GoogleChro...

工作流程:

1) 瀏覽器空閒時,獲取頁面所有a標籤的連結links;

2) 使用IntersectionObserver對link進行監聽;

3) 在視口區的link,使用prefetch下載;

4) 判斷當前網絡狀況,若使用的是2G或者開啟了省流模式(data-saver),則不做處理。

data-saver: The user may enable such preference, if made available by the user agent, due to high data transfer costs, slow connection speeds, or other reasons.

題外話:prefetch有點偷流量的意思,用戶想看什麼才消耗對應資源產生的流量,而prefetch則會擅自做主,偷偷下載很多可能並不需要的資源(在早前流量特貴的時候這麼做,估計會被罵...)

5) 下載連結資源,三種下載方式:fetch、xhr、

使用說明

  • 只支持prefetch;
  • 通過a標籤獲取links;
  • 最佳實踐是對後續可能訪問的頁面的提前下載,後續真正訪問時,直接從本地獲取執行。

總結

綜合來看,PreloadWebpackPlugin更適合對chunk而非html文件的處理;而quikLink更適合博客類的網站,或者服務端渲染的頁面,這樣才能實現"秒開"的預期效果。

參考文獻

  • preload-webpack-plugin
  • quicklink
  • IntersectionObserver
  • Preload, Prefetch And Priorities in Chrome
  • loadCSS
  • A Tale of Four Caches
  • React 16 加載性能優化指南
  • PWA直出
  • 億萬級訪問量下的前端同構直出實踐
文章來源: https://twgreatdaily.com/HvxwLXEBfwtFQPkd_l5T.html