【PWA學習與實踐】(10)使用Resource Hint提升頁面載入效能與體驗

AlienZHOU發表於2018-07-23

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

本文是《PWA學習與實踐》系列的第十篇文章。也許你還沒有聽說過或不瞭解Resource Hint,但是通過本文,你會快速學習到這一件頁面載入效能利器。本系列相關demo的程式碼都可以在github repo中找到。

PWA作為時下最火熱的技術概念之一,對提升Web應用的安全、效能和體驗有著很大的意義,非常值得我們去了解與學習。對PWA感興趣的朋友歡迎關注《PWA學習與實踐》系列文章。


對之前的文章感興趣的話,可以從這裡找到:


引言

我們知道,在沒有快取的情況下,無論是HTML、javascript還是一些API資料,頁面的每一個請求都需要從客戶端發起後經由服務端返回。在這種情況下,我們每一次涉及遠端請求的互動(開啟一個頁面、查詢列表資料、動態載入js指令碼等)都會有網路延遲。如果我們能夠預測或指定頁面預先進行一些網路操作,例如DNS解析或者預載入資源,那麼當我們在之後的操作中涉及到這部分資源時,載入會更迅速,互動也會更加流暢。

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

1. Resource Hint

Resource Hint是一系列相關標準,來告訴瀏覽器哪些源(origin)下的資源我們的Web App想要獲取,哪些資源在之後的操作或瀏覽時需要被使用,以便讓瀏覽器能夠進行一些預先連線或預先載入等操作。Resource Hint有DNS Prefetch、Preconnect、Prefetch和Prerender這四種。

1.1. DNS Prefetch

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

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>
複製程式碼

【PWA學習與實踐】(10)使用Resource Hint提升頁面載入效能與體驗

瀏覽器首先會去獲取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了。

【PWA學習與實踐】(10)使用Resource Hint提升頁面載入效能與體驗

更多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學習與實踐》系列

參考資料

相關文章