首發地址:https://mp.weixin.qq.com/s/8_...css
本文主要介紹前端性能優化中的資源預加載,不只會介紹常規的一些預加載手段;還會介紹工程實踐中的應用。 html
涉及內容:前端
前言node
當咱們須要某些網絡資源時,加載和執行每每耦合在一塊兒,下載完當即執行,而加載過程是阻塞式的,延長了onload時間。所以如何在資源執行前預加載資源,減小等待網絡的開銷即是咱們要探討的問題。webpack
1. 常規作法git
附一張不一樣資源瀏覽器優先級的圖示(來源):github
1)async/defer: 無阻塞加載
web
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="" />
使用說明
總結
綜合來看,PreloadWebpackPlugin更適合對chunk而非html文件的處理;而quikLink更適合博客類的網站,或者服務端渲染的頁面,這樣才能實現"秒開"的預期效果。
參考文獻
歡迎[ 掃碼 ]關注,最新文章,不按期更新~