《PWA學習與實踐》系列文章已整理至gitbook - PWA學習手冊,文字內(nèi)容已同步至learning-pwa-ebook巩搏。轉載請注明作者與出處缚柳。
本文是《PWA學習與實踐》系列的第十篇文章宙刘。也許你還沒有聽說過或不了解Resource Hint患朱,但是通過本文,你會快速學習到這一件頁面加載性能利器旭斥。本系列相關demo的代碼都可以在github repo中找到容达。
PWA作為時下最火熱的技術概念之一,對提升Web應用的安全垂券、性能和體驗有著很大的意義花盐,非常值得我們?nèi)チ私馀c學習。對PWA感興趣的朋友歡迎關注《PWA學習與實踐》系列文章。
對之前的文章感興趣的話算芯,可以從這里找到:
引言
我們知道,在沒有緩存的情況下近速,無論是HTML诈嘿、javascript還是一些API數(shù)據(jù),頁面的每一個請求都需要從客戶端發(fā)起后經(jīng)由服務端返回削葱。在這種情況下奖亚,我們每一次涉及遠程請求的交互(打開一個頁面、查詢列表數(shù)據(jù)析砸、動態(tài)加載js腳本等)都會有網(wǎng)絡延遲昔字。如果我們能夠預測或指定頁面預先進行一些網(wǎng)絡操作,例如DNS解析或者預加載資源首繁,那么當我們在之后的操作中涉及到這部分資源時作郭,加載會更迅速,交互也會更加流暢弦疮。
當然夹攒,目前已經(jīng)有一些技術手段來幫助我們實現(xiàn)資源的預加載,例如常見的使用XMLHttpRequest來獲取資源并進行緩存胁塞。然而咏尝,這些技術都是應用層面的,并非Web標準啸罢,某些需求也無法準確實現(xiàn)编检。同時,在性能方面也存在著問題扰才。好在目前已有相關的Web標準(Resource Hint)涉及到這一部分允懂,通過它,可以在瀏覽器原生層面實現(xiàn)這些功能衩匣,同時提供性能保證累驮。下面我們來了解一下Resource Hint相關技術。
1. Resource Hint
Resource Hint是一系列相關標準舵揭,來告訴瀏覽器哪些源(origin)下的資源我們的Web App想要獲取谤专,哪些資源在之后的操作或瀏覽時需要被使用,以便讓瀏覽器能夠進行一些預先連接或預先加載等操作午绳。Resource Hint有DNS Prefetch置侍、Preconnect、Prefetch和Prerender這四種。
1.1. DNS Prefetch
當我們在注重前端性能優(yōu)化時蜡坊,可能會忽略了DNS解析杠输。然而DNS的解析也是有耗時的。在Chrome的Timing Breakdown Phase中秕衙,第三階段就是DNS查詢蠢甲。DNS Prefetch就是幫助我們告知瀏覽器,某個源下的資源在之后會要被獲取据忘,這樣瀏覽器就會(Should)盡早解析它鹦牛。
Resource Hint主要通過使用link
標簽。rel
屬性確定類型勇吊,href
屬性則指定相應的源或資源URL曼追。DNS Prefetch可以像下面這樣使用:
<link rel="dns-prefetch" >
1.2. Preconnect
我們知道,建立連接不僅需要DNS查詢汉规,還需要進行TCP協(xié)議握手礼殊,有些還會有TLS/SSL協(xié)議,這些都會導致連接的耗時针史。因此晶伦,使用Preconnect可以幫助你告訴瀏覽器:“我有一些資源會用到某個源,可以幫我預先建立連接啄枕“颖瑁”
根據(jù)規(guī)范,當你使用Preconnect時射亏,瀏覽器大致做了如下處理:
- 首先近忙,解析Preconnect的URL
- 其次,根據(jù)當前l(fā)ink元素中的屬性進行cors的設置
- 默認先將credential設為true智润;如果cors為Anonymous并且存在跨域及舍,則將credential置為false
- 最后進行連接
使用Preconnect只需要將rel
屬性設為preconnect
即可:
<link rel="preconnect" >
當然,你也可以設置CORS
<link rel="preconnect" crossorigin>
需要注意的是窟绷,標準并沒有硬性規(guī)定瀏覽器一定要(而是SHOULD)完成整個連接過程锯玛,瀏覽器可以視情況完成部分工作。
1.3. Prefetch
你可以把Prefetch理解為資源預獲取兼蜈。一般來說攘残,可以用Prefetch來指定在緊接著之后的操作或瀏覽中需要使用到的資源,讓瀏覽器提前獲取为狸。由于僅僅是提前獲取資源歼郭,因此瀏覽器不會對資源進行預處理,并且像CSS樣式表辐棒、JavaScript腳本這樣的資源是不會自動執(zhí)行并應用于當前文檔的病曾。
需要注意的是牍蜂,和DNS Prefetch、Preconnect使用不太一樣的地方是泰涂,Prefetch有一個as
的可選屬性鲫竞,用來指定獲取資源的類型。由于不同的資源類型會具有不同的優(yōu)先級逼蒙、CSP从绘、請求頭等,因此該屬性很重要是牢。下表列出了一些常用資源的as
屬性值:
資源使用者 | 寫法 |
---|---|
<audio> |
<link rel=preload as=audio href=...> |
<video> |
<link rel=preload as=video href=...> |
<track> |
<link rel=preload as=track href=...> |
<script> , Worker's importScripts |
<link rel=preload as=script href=...> |
<link rel=stylesheet> , CSS @import |
<link rel=preload as=style href=...> |
CSS @font-face | <link rel=preload as=font href=...> |
<img> , <picture> , srcset, imageset |
<link rel=preload as=image href=...> |
SVG's <image> , CSS *-image |
<link rel=preload as=image href=...> |
XHR, fetch | <link rel=preload as=fetch crossorigin href=...> |
Worker, SharedWorker | <link rel=preload as=worker href=...> |
<embed> |
<link rel=preload as=embed href=...> |
<object> |
<link rel=preload as=object href=...> |
<iframe> , <frame>
|
<link rel=preload as=document href=...> |
HTML | <link rel=preload as=html href=...> |
可以看到僵井,Prefetch的可選資源類型非常豐富,除了我們常用的script
和style
妖泄,甚至還包括XHR、video艘策、img等蹈胡,基本涵蓋了Web中的各類資源。為了解決Prefetch中某些資源(例如XHR)的跨域問題朋蔫,可以為其應用CORS屬性罚渐。一個基本的Prefetch寫法也很簡單:
<link rel="prefetch" href="/my.little.script.js" as="script">
1.4. Prerender
上一部分我們講了Prefetch,而Prerender則是Prefetch的更進一步驯妄『刹ⅲ可以粗略地理解為“預處理”(預執(zhí)行)。
通過Prerender“預處理”的資源青扔,瀏覽器都會作為HTML進行處理源织。瀏覽器除了會去獲取資源,還可能會預處理(MAY preprocess)該資源微猖,而該HTML頁面依賴的其他資源谈息,像<script>
、<style>
等頁面所需資源也可能會被處理凛剥。但是預處理會由于瀏覽器或當前機器侠仇、網(wǎng)絡情況的不同而被不同程度地推遲。例如犁珠,會根據(jù)CPU逻炊、GPU和內(nèi)存的使用情況,以及請求操作的冪等性而選擇不同的策略或阻止該操作犁享。
注意余素,由于這些預處理操作的不可控性,當你只是需要能夠預先獲取部分資源來加速后續(xù)可能出現(xiàn)的網(wǎng)絡請求時炊昆,建議使用Prefetch溺森。當使用Prerender時慕爬,為了保證兼容性,目標頁面可以監(jiān)聽visibilitychange
事件并使用document.visibilityState
來判斷頁面狀態(tài)屏积。
When prerendering a document the user agent MUST set the document's visibilityState value to prerender. —— W3C Working Draft
Prerender的使用方式非常簡單医窿,與DNS Prefetch和Preconnect類似,指定rel
屬性為prerender
:
<link rel="prerender" >
2. Resource Hint的具體使用方式
在上面的部分里炊林,我主要介紹了DNS Prefetch姥卢、Preconnect、Prefetch和Prerender這四種RHL(Resource Hint Link)渣聚,并且簡單介紹了如何在link
中使用它們独榴。然而除了直接在HTML中加入對應link
標簽外,還可以通過其他幾種方式觸發(fā)瀏覽器的Resource Hint奕枝。為了更加直觀棺榔,下面我們還是以圖書搜索這個demo為例來看看可以通過哪些方法來使用Resource Hint。
假設已經(jīng)為該demo添加詳情頁
nextpage.html
及其依賴的nextpage.js
隘道,當點擊列表中的圖書時會進行跳轉症歇。
2.1. 文檔head中的link元素
這是Resource Hint最常用的一種方式,我們上面介紹的各種示例也就是使用的這種方式谭梗。例如想要指定Prefetch nextpage.js
腳本可以這么寫:
<link rel="prefetch" href="./nextpage.js" as="script">
2.2. HTTP Link頭字段
可以通過Link HTTP header來使用Resource Hint忘晤。Link HTTP header和link
元素是等價的。
The Link entity-header field provides a means for serialising one or more links in HTTP headers. It is semantically equivalent to the <LINK> element in HTML, as well as the atom:link feed-level element in Atom. —— RFC5988
Link
主要由兩部分組成——URI-Reference
和link-param
激捏。URI-Reference
相當于link
元素中的href
屬性设塔;link-param
則包括了rel
、title
远舅、type
等一系列元素屬性闰蛔,使用;
分割。因此可以在響應頭中添加以下部分:
Link: </nextpage.js>; rel="prefetch"; as="script"
我們的demo使用了koa-static這個中間件图柏,只要做如下修改即可:
// app.js
app.use(serve(__dirname + '/public', {
maxage: 1000 * 60 * 60,
setHeaders: (res, path, stats) => {
if (/index.html/.test(path)) {
res.setHeader('Link', '</nextpage.js>; rel="prefetch"; as="script"');
}
}
}));
你會發(fā)現(xiàn)钞护,在訪問index.html
時,瀏覽器就會向服務器請求nextpage.js
這個頁面本身并“不需要”用到的資源爆办。
2.3. 向文檔動態(tài)添加link元素
link
元素也支持我們通過js動態(tài)向文檔添加难咕。對于動態(tài)添加的RHL,瀏覽器也會對其應用Resource Hint策略距辆。添加link
的方式和添加普通dom元素一致余佃。
var hint = document.createElement('link');
hint.rel = 'prefetch';
hint.as = 'script';
hint.href = '/nextpage.js';
document.head.appendChild(hint);
2.4. 改變已有l(wèi)ink元素的href屬性
當你改變頁面中原有RHL的href
屬性(或者prefetch時的as
屬性)時,會立即觸發(fā)對新資源的Resource Hint跨算。例如在如下代碼執(zhí)行后
var hint = document.querySelector('[rel="prefetch"]');
hint.href = './the.other.nextpage.js';
瀏覽器相當于接收到了新的Resource Hint“指示”爆土,并在合適的時機向服務端請求the.other.nextpage.js
這個資源。注意诸蚕,當你修改as
屬性時步势,也會觸發(fā)Resource Hint氧猬。
注意,如果你想通過修改已有link
元素預獲取nextpage.html
這個資源坏瘩,然后像下面這樣寫會觸發(fā)兩次請求盅抚。
var hint = document.querySelector('[rel="prefetch"]');
hint.as = 'html'; // 觸發(fā)第一次請求,再次請求./nextpage.js
hint.href = './nextpage.html'; // 請求./nextpage.html
2. Preload
既然提到了Resource Hint倔矾,那么不得不介紹一下與其類似的Preload妄均。在遇到需要Preload的資源時,瀏覽器會 立刻 進行預獲取哪自,并將結果放在內(nèi)存中丰包,資源的獲取不會影響頁面parse與load事件的觸發(fā)。直到再次遇到該資源的使用標簽時壤巷,才會執(zhí)行邑彪。
(Preload) Initiating an early fetch and separating fetching from resource execution.
例如下面這個HTML片段:
<head>
<link rel="preload" href="./nextpage.js" as="script">
<script type="text/javascript" src="./current.js"></script>
<script type="text/javascript" src="./nextpage.js"></script>
<head>
瀏覽器首先會去獲取nextpage.js
,然后獲取并執(zhí)行current.js
胧华,最后寄症,遇到使用nextpage.js
資源的script
標簽時,將已經(jīng)獲取的nextpage.js
執(zhí)行撑柔。由于我們會將script
標簽置于body底部來保證性能瘸爽,因此可以考慮在head標簽中添加這些資源的Preload來加速頁面的加載與渲染您访。
更進一步铅忿,我們還可以監(jiān)聽Preload的情況,并觸發(fā)自定以操作
<script>
function preloadFinished(e) { ... }
function preloadError(e) { ... }
</script>
<!-- listen for load and error events -->
<link rel="preload" href="app.js" as="script" onload="preloadFinished()" onerror="preloadError()">
正如在引言中所提到的灵汪,在過去如果我們想預加載一些資源都會用一些應用層面的技術手段檀训,但往往會遇到兩個問題:
- 我們需要先獲取資源,然后在適當時執(zhí)行享言,但兩者并不易于分離
- 無論哪種技術實現(xiàn)峻凫,都會帶來一定的性能與體驗損傷
Preload(包括前文提到的Prefetch等RHL)給我們帶來的價值就是從瀏覽器層面很好地將資源的加載與執(zhí)行分離了,并在瀏覽器層面來保證良好的性能體驗览露。
看到這里荧琼,也許你會疑惑,都是會預獲取資源差牛,都是資源的獲取與執(zhí)行分離命锄,那么Preload與Prefetch有什么區(qū)別呢?
這是它最容易與Prefetch混淆的地方偏化。在標準里有這么一段話解釋兩者區(qū)別:
The application can use the preload keyword to initiate <u>early, high-priority, and non-render-blocking</u> fetch of a CSS resource that can then be applied by the application at appropriate time
與Prefetch相比脐恩,Preload會強制瀏覽器立即獲取資源,并且該請求具有較高的優(yōu)先級(mandatory and high-priority)侦讨,因此建議對一些當前頁面會馬上用到資源使用Preload驶冒;相對的苟翻,Prefetch的資源獲取則是可選與較低優(yōu)先級的,其是否獲取完全取決于瀏覽器的決定骗污,適用于預獲取將來可能會用到的資源崇猫。
為了節(jié)省不必要的帶寬消耗,如果Preload的資源在3s內(nèi)沒有被使用身堡,Chrome控制臺會出現(xiàn)類似下圖的警告邓尤。這時你就需要仔細思考,該資源是否有必要Preload了贴谎。
更多Preload與Prefetch的細節(jié)差異可以看這里 —— Preload, Prefetch And Priorities in Chrome汞扎。
3. 寫在最后
本文介紹了如何使用Resource Hint(以及Preload)來提升頁面加載性能與體驗,簡單來說:
- DNS Prefetch 可以幫助我們進行DNS預查詢擅这;
- Preconnect 可以幫助我們進行預連接澈魄,例如在一些重定向技術中,可以讓瀏覽器和最終目標源更早建立連接仲翎;
- Prefetch 可以幫助我們預先獲取所需資源(并且不用擔心該資源會被執(zhí)行)痹扇,例如我們可以根據(jù)用戶行為猜測其下一步操作,然后動態(tài)預獲取所需資源溯香;
- Prerender 則會更進一步鲫构,不僅獲取資源,還會預加載(執(zhí)行)部分資源玫坛,因此如果我們Prerender下一個頁面结笨,打開該頁面時會讓用戶感覺非常流暢;
- Preload 則像是 Prefetch的升級版湿镀,會強制立即高優(yōu)獲取資源炕吸,非常適合Preload(盡早獲取)一些關鍵渲染路徑中的資源勉痴。
雖然赫模,大部分PWA相關資料中并不會提及Resource Hint,但是正如我在第一篇文章中提到的
PWA本身其實是一個概念集合蒸矛,它不是指某一項技術瀑罗,而是通過一系列的Web技術與Web標準來優(yōu)化Web App的安全、性能和體驗雏掠。
Resource Hint顯然符合這一點斩祭。
我們不應該將PWA局限在Service Worker離線緩存、提醒通知這些常見的PWA內(nèi)容中磁玉,希望讀者也能開闊思維停忿,理解PWA背后的概念與思想。因此蚊伞,在后續(xù)文章中我也會介紹前端存儲(sessionStorage/localStorage/indexDB)席赂、HTTP/2.0以及PWA進展等相關內(nèi)容吮铭。
在下一篇里,我們會一起來學習Google開源的PWA離線工具集 —— workbox颅停。通過workbox谓晌,我們可以學習<u>各類離線策略</u>,并且了解一些生產(chǎn)環(huán)境中需要考慮的問題癞揉。部分開源PWA解決方案也是基于workbox進行封裝的纸肉。
《PWA學習與實踐》系列
- 第一篇:2018,開始你的PWA學習之旅
- 第二篇:10分鐘學會使用Manifest喊熟,讓你的WebApp更“Native”
- 第三篇:從今天起柏肪,讓你的WebApp離線可用
- 第四篇:TroubleShooting: 解決FireBase login驗證失敗問題
- 第五篇:與你的用戶保持聯(lián)系: Web Push功能
- 第六篇:How to Debug? 在chrome中調(diào)試你的PWA
- 第七篇:增強交互:使用Notification API來進行提醒
- 第八篇:使用Service Worker進行后臺數(shù)據(jù)同步
- 第九篇:PWA實踐中的問題與解決方案
- 第十篇:Resource Hint - 提升頁面加載性能與體驗(本文)
參考資料
- Resource Hints W3C Working Draft 15 January 2018
- Preload W3C Candidate Recommendation 26 October 2017
- Preload, Prefetch And Priorities in Chrome
- Web Linking
- Page Visibility Level 2 W3C Proposed Recommendation 17 October 2017
- CORS settings attributes
- Content Security Policy Level 3 W3C Working Draft, 13 September 2016