資源預加載 - 性能優化需知~

首發地址:https://mp.weixin.qq.com/s/8_...css

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

涉及內容:前端

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

前言node

    當咱們須要某些網絡資源時,加載和執行每每耦合在一塊兒,下載完當即執行,而加載過程是阻塞式的,延長了onload時間。所以如何在資源執行前預加載資源,減小等待網絡的開銷即是咱們要探討的問題。webpack

1. 常規作法git

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

1)async/defer: 無阻塞加載
async.pngweb

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

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

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

2)使用ajax加載資源:能夠實現預加載。

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

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

    原始解析作法:

    

    採用預解析(掃描)器:

    

不足:

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

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

4)Server Push   圖片來源

資源推送:

Link: <https://example.com/other/styles.css>; rel=preload; as=style;

資源僅預加載,不推送:

Link: <https://example.com/other/styles.css>; 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');
return relList && relList.supports && relList.supports('preload');

如何使用

//link標籤
<link rel="preload"  as="style" herf="./a.css"/>
<link rel="prefetch" as="script" href="./b.js"/>

//動態建立
let preLink = document.createElement('link');
preLink.rel ='prefetch';  //感受動態建立不適合preload
prelink.as = 'script';
preLink.href = './a.js';

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頁面中應用:
//head中
<link rel="preload" href="https://abs.twimg.com/k/zh-cn/init.zh-cn.3b38ddbf651139df6007.js" as="script">
//body底部
<script src="https://abs.twimg.com/k/zh-cn/init.zh-cn.3b38ddbf651139df6007.js" async></script>

preload的polyfill

背景知識

//若支持preload,異步下載完不會當即執行
<link rel="preload" >
//下載完當即應用到DOM樹
<link rel="stylesheet" >
//異步下載,只有打印的時候纔會應用,不符合則不會應用,所以不會阻塞渲染
<link rel="stylesheet" media='print' >

polyfill實現思路

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

// 1. 【支持preload的狀況】
//因爲preload只是獲取樣式,不會當即應用,所以使用onload改變link的rel使其當即生效9)
<link rel="preload" href="style.css" as="style" onload="this.onload = null;this.rel ='stylesheet'">
注:設置onload=null主要是由於有些瀏覽器會在rel改變時再次出發load事件。

// 2. 【不支持preload的狀況:核心仍是media的使用】
// 1)獲取所有link
let links = document.getElementsByTagName("link");
// 2)緩存每一個link的media
var finalMedia = link.media || "all";
// 3)改變link的rel和media(異步下載但不會應用)
link.rel = "stylesheet";
link.media = "only x";
// 4)若是綁定onload事件(爲了啓用media)
if( link.addEventListener ){
link.addEventListener( "load", enableStylesheet );
} else if( link.attachEvent ){
link.attachEvent( "onload", enableStylesheet );
}
// 5)爲了應對舊的瀏覽器不支持link的onload事件
setTimeout( enableStylesheet, 3000 );
// 6)enableStylesheet回調中作以下操:將media恢復,樣式當即應用
link.setAttribute( "onload", null ); 
link.media = finalMedia;

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

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

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

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

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

  2)HTTP/2的serverPush

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

結合webpack的實踐

1. 使用插件:PreloadWebpackPlugin

經常使用的配置以下:

new PreloadWebpackPlugin({
   //preload or  prefetch方式
   rel: '',
   /*
    *即<link as='' />中的as,代表資源類型,不一樣的類型決定了不一樣的執行優先級
    *好比:script的優先級大於style
   */
   as: '',
  //排除的html頁面集合,即只關聯要配置的頁面
   excludeHtmlNames: [],
   //所關聯頁面須要使用preload或prefetch的資源
   include: []
})

其中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鉤子將構造的<link>插入html中:

plugins: [
  new HtmlWebpackPlugin(),
  new PreloadWebpackPlugin()
]

使用效果

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

<link as="script" href="/common.js" rel="preload">
<link  as\="script"  href\="/asyncChunk.js"  rel\="prefetch"\>

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

2. 結合import()

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

若是工程沒有使用HtmlWebpackPlugin,能夠對動態導入的資源作以下處理:

import(/* webpackPrefetch: true */)
import(/\* webpackPreload: true \*/)

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

quiklink

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

工做原理

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

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

let target= document.getElementById('a');
io = new IntersectionObserver(
    entries => {}, 
    {
      threshold: [1]  //交叉區域爲1時會觸發callback
    }
);
io.observe(target);

【備註】常規的主要是經過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、<link rel=prefetch href="" />

使用說明

  • 只支持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直出
  • 億萬級訪問量下的前端同構直出實踐

歡迎[ 掃碼 ]關注,最新文章,不按期更新~

    

相關文章
相關標籤/搜索