【PWA學習與實踐】(10)使用Resource Hint提高頁面加載性能與體驗

《PWA學習與實踐》系列文章已整理至gitbook - PWA學習手冊,文字內容已同步至learning-pwa-ebook。轉載請註明做者與出處。javascript

本文是《PWA學習與實踐》系列的第十篇文章。也許你尚未據說過或不瞭解Resource Hint,可是經過本文,你會快速學習到這一件頁面加載性能利器。本系列相關demo的代碼均可以在github repo中找到。html

PWA做爲時下最火熱的技術概念之一,對提高Web應用的安全、性能和體驗有着很大的意義,很是值得咱們去了解與學習。對PWA感興趣的朋友歡迎關注《PWA學習與實踐》系列文章。前端


對以前的文章感興趣的話,能夠從這裏找到:java


引言

咱們知道,在沒有緩存的狀況下,不管是HTML、javascript仍是一些API數據,頁面的每個請求都須要從客戶端發起後經由服務端返回。在這種狀況下,咱們每一次涉及遠程請求的交互(打開一個頁面、查詢列表數據、動態加載js腳本等)都會有網絡延遲。若是咱們可以預測或指定頁面預先進行一些網絡操做,例如DNS解析或者預加載資源,那麼當咱們在以後的操做中涉及到這部分資源時,加載會更迅速,交互也會更加流暢。git

固然,目前已經有一些技術手段來幫助咱們實現資源的預加載,例如常見的使用XMLHttpRequest來獲取資源並進行緩存。然而,這些技術都是應用層面的,並不是Web標準,某些需求也沒法準確實現。同時,在性能方面也存在着問題。好在目前已有相關的Web標準(Resource Hint)涉及到這一部分,經過它,能夠在瀏覽器原生層面實現這些功能,同時提供性能保證。下面咱們來了解一下Resource Hint相關技術。github

1. Resource Hint

Resource Hint是一系列相關標準,來告訴瀏覽器哪些源(origin)下的資源咱們的Web App想要獲取,哪些資源在以後的操做或瀏覽時須要被使用,以便讓瀏覽器可以進行一些預先鏈接或預先加載等操做。Resource Hint有DNS Prefetch、Preconnect、Prefetch和Prerender這四種。web

1.1. DNS Prefetch

當咱們在注重前端性能優化時,可能會忽略了DNS解析。然而DNS的解析也是有耗時的。在Chrome的Timing Breakdown Phase中,第三階段就是DNS查詢。DNS Prefetch就是幫助咱們告知瀏覽器,某個源下的資源在以後會要被獲取,這樣瀏覽器就會(Should)儘早解析它。chrome

Resource Hint主要經過使用link標籤。rel屬性肯定類型,href屬性則指定相應的源或資源URL。DNS Prefetch能夠像下面這樣使用:跨域

<link rel="dns-prefetch" href="//yourwebsite.com">
複製代碼

1.2. Preconnect

咱們知道,創建鏈接不只須要DNS查詢,還須要進行TCP協議握手,有些還會有TLS/SSL協議,這些都會致使鏈接的耗時。所以,使用Preconnect能夠幫助你告訴瀏覽器:「我有一些資源會用到某個源,能夠幫我預先創建鏈接。」瀏覽器

根據規範,當你使用Preconnect時,瀏覽器大體作了以下處理:

  • 首先,解析Preconnect的URL
  • 其次,根據當前link元素中的屬性進行cors的設置
  • 默認先將credential設爲true;若是cors爲Anonymous而且存在跨域,則將credential置爲false
  • 最後進行鏈接

使用Preconnect只須要將rel屬性設爲preconnect便可:

<link rel="preconnect" href="//yourwebsite.com">
複製代碼

固然,你也能夠設置CORS

<link rel="preconnect" href="//yourwebsite.com" crossorigin>
複製代碼

須要注意的是,標準並無硬性規定瀏覽器必定要(而是SHOULD)完成整個鏈接過程,瀏覽器能夠視狀況完成部分工做。

1.3. Prefetch

你能夠把Prefetch理解爲資源預獲取。通常來講,能夠用Prefetch來指定在緊接着以後的操做或瀏覽中須要使用到的資源,讓瀏覽器提早獲取。因爲僅僅是提早獲取資源,所以瀏覽器不會對資源進行預處理,而且像CSS樣式表、JavaScript腳本這樣的資源是不會自動執行並應用於當前文檔的。

須要注意的是,和DNS Prefetch、Preconnect使用不太同樣的地方是,Prefetch有一個as的可選屬性,用來指定獲取資源的類型。因爲不一樣的資源類型會具備不一樣的優先級、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的可選資源類型很是豐富,除了咱們經常使用的scriptstyle,甚至還包括XHR、video、img等,基本涵蓋了Web中的各種資源。爲了解決Prefetch中某些資源(例如XHR)的跨域問題,能夠爲其應用CORS屬性。一個基本的Prefetch寫法也很簡單:

<link rel="prefetch" href="/my.little.script.js" as="script">
複製代碼

1.4. Prerender

上一部分咱們講了Prefetch,而Prerender則是Prefetch的更進一步。能夠粗略地理解爲「預處理」(預執行)。

經過Prerender「預處理」的資源,瀏覽器都會做爲HTML進行處理。瀏覽器除了會去獲取資源,還可能會預處理(MAY preprocess)該資源,而該HTML頁面依賴的其餘資源,像<script><style>等頁面所需資源也可能會被處理。可是預處理會因爲瀏覽器或當前機器、網絡狀況的不一樣而被不一樣程度地推遲。例如,會根據CPU、GPU和內存的使用狀況,以及請求操做的冪等性而選擇不一樣的策略或阻止該操做。

注意,因爲這些預處理操做的不可控性,當你只是須要可以預先獲取部分資源來加速後續可能出現的網絡請求時,建議使用Prefetch。當使用Prerender時,爲了保證兼容性,目標頁面能夠監聽visibilitychange事件並使用document.visibilityState來判斷頁面狀態。

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" href="//yourwebsite.com/nextpage.html">
複製代碼

2. Resource Hint的具體使用方式

在上面的部分裏,我主要介紹了DNS Prefetch、Preconnect、Prefetch和Prerender這四種RHL(Resource Hint Link),而且簡單介紹瞭如何在link中使用它們。然而除了直接在HTML中加入對應link標籤外,還能夠經過其餘幾種方式觸發瀏覽器的Resource Hint。爲了更加直觀,下面咱們仍是以圖書搜索這個demo爲例來看看能夠經過哪些方法來使用Resource Hint。

假設已經爲該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 element in HTML, as well as the atom:link feed-level element in Atom. —— RFC5988

Link主要由兩部分組成——URI-Referencelink-paramURI-Reference至關於link元素中的href屬性;link-param則包括了reltitletype等一系列元素屬性,使用;分割。所以能夠在響應頭中添加如下部分:

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"');
        }
    }
}));
複製代碼

你會發現,在訪問index.html時,瀏覽器就會向服務器請求nextpage.js這個頁面自己並「不須要」用到的資源。

2.3. 向文檔動態添加link元素

link元素也支持咱們經過js動態向文檔添加。對於動態添加的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. 改變已有link元素的href屬性

當你改變頁面中原有RHL的href屬性(或者prefetch時的as屬性)時,會當即觸發對新資源的Resource Hint。例如在以下代碼執行後

var hint = document.querySelector('[rel="prefetch"]');
hint.href = './the.other.nextpage.js';
複製代碼

瀏覽器至關於接收到了新的Resource Hint「指示」,並在合適的時機向服務端請求the.other.nextpage.js這個資源。注意,當你修改as屬性時,也會觸發Resource Hint。

注意,若是你想經過修改已有link元素預獲取nextpage.html這個資源,而後像下面這樣寫會觸發兩次請求。

var hint = document.querySelector('[rel="prefetch"]');
hint.as = 'html'; // 觸發第一次請求,再次請求./nextpage.js
hint.href = './nextpage.html'; // 請求./nextpage.html
複製代碼

2. Preload

既然提到了Resource Hint,那麼不得不介紹一下與其相似的Preload。在遇到須要Preload的資源時,瀏覽器會 馬上 進行預獲取,並將結果放在內存中,資源的獲取不會影響頁面parse與load事件的觸發。直到再次遇到該資源的使用標籤時,纔會執行。

(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,而後獲取並執行current.js,最後,遇到使用nextpage.js資源的script標籤時,將已經獲取的nextpage.js執行。因爲咱們會將script標籤置於body底部來保證性能,所以能夠考慮在head標籤中添加這些資源的Preload來加速頁面的加載與渲染。

更進一步,咱們還能夠監聽Preload的狀況,並觸發自定以操做

<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()">
複製代碼

正如在引言中所提到的,在過去若是咱們想預加載一些資源都會用一些應用層面的技術手段,但每每會遇到兩個問題:

  • 咱們須要先獲取資源,而後在適當時執行,但二者並不易於分離
  • 不管哪一種技術實現,都會帶來必定的性能與體驗損傷

Preload(包括前文提到的Prefetch等RHL)給咱們帶來的價值就是從瀏覽器層面很好地將資源的加載與執行分離了,並在瀏覽器層面來保證良好的性能體驗。

看到這裏,也許你會疑惑,都是會預獲取資源,都是資源的獲取與執行分離,那麼Preload與Prefetch有什麼區別呢?

這是它最容易與Prefetch混淆的地方。在標準裏有這麼一段話解釋二者區別:

The application can use the preload keyword to initiate early, high-priority, and non-render-blocking fetch of a CSS resource that can then be applied by the application at appropriate time

與Prefetch相比,Preload會強制瀏覽器當即獲取資源,而且該請求具備較高的優先級(mandatory and high-priority),所以建議對一些當前頁面會立刻用到資源使用Preload;相對的,Prefetch的資源獲取則是可選與較低優先級的,其是否獲取徹底取決於瀏覽器的決定,適用於預獲取未來可能會用到的資源。

爲了節省沒必要要的帶寬消耗,若是Preload的資源在3s內沒有被使用,Chrome控制檯會出現相似下圖的警告。這時你就須要仔細思考,該資源是否有必要Preload了。

更多Preload與Prefetch的細節差別能夠看這裏 —— Preload, Prefetch And Priorities in Chrome

3. 寫在最後

本文介紹瞭如何使用Resource Hint(以及Preload)來提高頁面加載性能與體驗,簡單來講:

  • DNS Prefetch 能夠幫助咱們進行DNS預查詢;
  • Preconnect 能夠幫助咱們進行預鏈接,例如在一些重定向技術中,可讓瀏覽器和最終目標源更早創建鏈接;
  • Prefetch 能夠幫助咱們預先獲取所需資源(而且不用擔憂該資源會被執行),例如咱們能夠根據用戶行爲猜想其下一步操做,而後動態預獲取所需資源;
  • Prerender 則會更進一步,不只獲取資源,還會預加載(執行)部分資源,所以若是咱們Prerender下一個頁面,打開該頁面時會讓用戶感受很是流暢;
  • Preload 則像是 Prefetch的升級版,會強制當即高優獲取資源,很是適合Preload(儘早獲取)一些關鍵渲染路徑中的資源。

雖然,大部分PWA相關資料中並不會說起Resource Hint,可是正如我在第一篇文章中提到的

PWA自己實際上是一個概念集合,它不是指某一項技術,而是經過一系列的Web技術與Web標準來優化Web App的安全、性能和體驗。

Resource Hint顯然符合這一點。

咱們不該該將PWA侷限在Service Worker離線緩存、提醒通知這些常見的PWA內容中,但願讀者也能開闊思惟,理解PWA背後的概念與思想。所以,在後續文章中我也會介紹前端存儲(sessionStorage/localStorage/indexDB)、HTTP/2.0以及PWA進展等相關內容。

在下一篇裏,咱們會一塊兒來學習Google開源的PWA離線工具集 —— workbox。經過workbox,咱們能夠學習各種離線策略,而且瞭解一些生產環境中須要考慮的問題。部分開源PWA解決方案也是基於workbox進行封裝的。

《PWA學習與實踐》系列

參考資料

相關文章
相關標籤/搜索