<link>標籤實現預載入功能詳解

ZKL發表於2018-06-15

前言

最近在研究 vue-cli 3.0生成的工程,在構建後生成的 index.html裡面發現了下面這種用法:

<link as=style href=/css/app.f60416c7.css rel=preload>複製程式碼
<link as=script href=/js/app.69189fdd.js rel=preload>複製程式碼

這就觸到了本人的知識盲區了,本著掃盲的目的,研究了下 link 標籤,發現這個小東西功能還是挺強大的,上面的就是為了實現預載入功能,懂點兒英文的,一看見preload 就大致知道了。

之前也有預載入技術,像 prefetch,subresource 等,關於這兩者和 preload 的區別,這是另外的話題了, 感興趣的可以自己搜一下,不想搜的,你只要知道這兩個跟 preload 相比弱的一逼就行了,就是 prefetch 瀏覽器相容性方面稍微好一點點,這三個也各有偏重和應用場景,就不詳細介紹了,下面我們就詳細展開preload 這塊。

功能介紹:

preload 是一項新的 web 標準,旨在提高效能,讓 FE 對載入的控制更加粒度化。它讓開發者有自定義載入邏輯的能力,免受基於指令碼的載入器所帶來的效能損耗。

preload 一個基本的用法就是提前載入資源,儘管大多數基於標記語言的資源能被瀏覽器的預載入器(preloader)儘早發現,但不是所有的資源都是基於標記語言的,比如一些隱藏在 css 和 js 中的資源(字型,圖片等),當瀏覽器發現頁面需要這些資源時,重新走一遍載入執行渲染的過程,會降低使用者體驗,並且對頁面的渲染 造成延遲;


Preloader 簡介
HTML 解析器在建立 DOM 時如果碰上同步指令碼(synchronous script),解析器會停止建立 DOM,轉而去執行指令碼。所以,如果資源的獲取只發生在解析器建立 DOM時,同步指令碼的介入將使網路處於空置狀態,尤其是對外部指令碼資源來說,當然,頁面內的指令碼有時也會導致延遲。


預載入器(Preloader)的出現就是為了優化這個過程,預載入器通過分析瀏覽器對 HTML 文件的早期解析結果(這一階段叫做“令牌化(tokenization)”),找到可能包含資源的標籤(tag),並將這些資源的 URL 收集起來。令牌化階段的輸出將會送到真正的 HTML 解析器手中,而收集起來的資源 URLs 會和資源型別一起被送到讀取器(fetcher)手中,讀取器會根據這些資源對頁面載入速度的影響進行有次序地載入。


預載入的好處:

  1. 讓瀏覽器提前載入指定資源(這裡預載入完成後並不執行),在需要執行的時候在執行,這樣將載入和執行分開,可以不阻塞渲染和 window.onload事件。
  1. 提前預載入指定資源,特別是字型檔案,不會再出現 font 字型在頁面渲染出來後,才載入完畢,然後頁面字型閃一下變成預期字型。
  1. 帶有 onload 事件,可以自定義資源在預載入完畢後的回撥函式。

涉及屬性介紹:

屬性名

取值範圍

介紹

as

script:js 指令碼font:字型檔案style:樣式表audio:音訊video: 視訊document:將被嵌入到<frame>或<iframe>元素內部的頁面image: 圖片fetch:將要通過 fetch 和 XHR 請求獲取的資源比如jsonobject: 將被嵌入到<embed >元素內的檔案worker:js 的 web worker 或 share worker

該屬性僅在 link 元素設定了rel=preload 是才能使用。規定了 link 元素要預載入的資源的型別,其取值範圍也限制了哪些資源才可被預載入。設定了此屬性使瀏覽器能夠:1,更精確地優化資源載入優先順序。2,匹配未來的載入需求,在適當的情況下,重複利用同一資源。3,為資源應用正確的內容安全策略。4,為資源設定正確的 Accept 請求頭。

href

<url>

指定要載入資源的 URL,可以使絕對地址也可以是相對地址

rel

preload(當前功能相關)

此屬性用於指明被連結的資源相對於當前頁面的關係。屬性值一定是被空格分開的連結型別值。這個屬性最常見的取值是:stylesheet,表明被連線資源對當前文件來說是一個層疊樣式表。當前取值 preload 表明連結資源是一個預載入的資源

type

MIME涵蓋型別

連結資源的 MIME 型別,在瀏覽器進行預載入到時候,這個屬性將會非常有用,瀏覽器將使用 type 屬性來判斷它是否支援這一資源型別,如果支援,將正常預載入,下載將開始,否則對其忽略。

crossorigin

載入字型檔案的時候需要用到,詳情往下看

應用場景:

  • 包含媒體

<link>元素有一個很棒的特性是它們能夠接受一個media屬性。它們可以接受媒體型別或有效的媒體查詢作為屬性值,這將令你能夠使用響應式的預載入!

讓我們來看一個簡單的示例(可以檢視Github上的原始碼線上示例):

<head>  <meta charset="utf-8">  <title>Responsive preload example</title>  <link rel="preload" href="bg-image-narrow.png" as="image" media="(max-width: 600px)">  <link rel="preload" href="bg-image-wide.png" as="image" media="(min-width: 601px)">  <link rel="stylesheet" href="main.css"></head><body>  <header>    <h1>My site</h1>  </header>  <script>    var mediaQueryList = window.matchMedia("(max-width: 600px)");    var header = document.querySelector('header');    if(mediaQueryList.matches) {      header.style.backgroundImage = 'url(bg-image-narrow.png)';    } else {      header.style.backgroundImage = 'url(bg-image-wide.png)';    }  </script></body>複製程式碼

你可以看到我們在<link>元素中包含了一個media屬性,因此,當使用者在使用較窄螢幕的裝置時,較窄的圖片將會被預載入,而在較寬的裝置上,較寬的圖片將被預載入。然後我們仍需要在header元素上附加合適的圖片——通過Window.matchMedia / MediaQueryList 來加以實現(可以檢視Testing media queries一文來了解更多資訊)。


  • 字型提前載入

web 字型是較晚才能被發現的關鍵資源中常見的一種。但是在使用者體驗對前端來說至關重要的現階段前端開發來說,web 字型對頁面的渲染也是至關重要。字型的引用被深埋在 css 中,即便預載入器有提前解析 css,也無法確定包含字型資訊的選擇器是否會真正作用在 dom 節點上。所以為了減少 FOUT(無樣式字型閃爍,flash of unstyled text )需要預載入字型檔案,有了 preload,一行程式碼搞定:

<link rel=preload href='font.woff2' as=font type='font/woff2' crossorigin />複製程式碼
NOTE :
crossorigin 屬性在載入字型的時候是必須的,即便字型沒有跨域是在自己公司的伺服器上,因為使用者代理必須採用匿名模式來獲取字型資源(為什麼會這樣呢?)。
type 屬性可以確保瀏覽器只獲取自己支援的資源。


  • 動態載入,但不執行

另外一個有意思的場景也因為 preload 的出現變得可能——當你想載入某一資源但卻不想執行它。比如說,你想在頁面生命週期的某一時刻執行一段指令碼,而你無法對這段指令碼做任何修改,不可能為它建立一個所謂的 runNow()函式。


在 preload 出現之前,你能做的很有限。如果你的方法是在希望指令碼執行的位置插入指令碼,由於指令碼只有在載入完成以後才能被瀏覽器執行,也就是說你得等上一會兒。如果採用 XHR 提前載入指令碼,瀏覽器會拒絕重用這段指令碼,有些情況下,你可以使用 eval 函式來執行這段指令碼,但該方法並不總是行得通,也不是完全沒有副作用。

現在有了 preload,一切變得可能

var link = document.createElement("link");複製程式碼
link.href = "myscript.js";複製程式碼
link.rel = "preload";複製程式碼
link.as = "script";複製程式碼
document.head.appendChild(link);複製程式碼

上面這段程式碼可以讓你預先載入指令碼,下面這段程式碼可以讓指令碼執行

var script = document.createElement("script");複製程式碼
script.src = "myscript.js";複製程式碼
document.body.appendChild(script);
複製程式碼
  • 基於標記語言的非同步載入

先看程式碼

<link rel="preload" as="style" href="asyncstyle.css" onload="this.rel='stylesheet'">複製程式碼

preload 的 onload 事件可以在資源載入完成後修改 rel 屬性,從而實現非常酷的非同步資源載入。

指令碼也可以採用這種方法實現非同步載入

難道我們不是已經有了<script async>? <scirpt async>雖好,但卻會阻塞 window 的 onload 事件。某些情況下,你可能希望這樣,但總有一些情況你不希望阻塞 window 的 onload 。

舉個例子,你想盡可能快的載入一段統計頁面訪問量的程式碼,但又不願意這段程式碼的載入給頁面渲染造成延遲從而影響使用者體驗,關鍵是,你不想延遲 window 的 onload 事件。

有了preload, 分分鐘搞定。

<link rel="preload" as="script" href="async_script.js"複製程式碼
      onload="var script = document.createElement('script'); script.src = this.href; document.body.appendChild(script);">複製程式碼
  • 響應式載入

preload 是一個link,根據規範有一個media 屬性(現在 Chrome 還不支援,不過快了),該屬性使得選擇性載入成為可能。

有什麼用處呢?假設你的站點同時支援桌面和移動端的訪問,在使用桌面瀏覽器訪問時,你希望呈現一張可互動的大地圖,而在移動端,一張較小的靜態地圖就足夠了。

你肯定不想同時載入兩個資源,現在常見的做法是通過 JS 判斷當前瀏覽器型別動態地載入資源,但這樣一來,瀏覽器的預載入器就無法及時發現他們,可能耽誤載入時機,影響使用者體驗和 SpeedIndex 評分。

怎樣才能讓瀏覽器儘可能早的發現這些資源呢?還是 Preload!

通過 Preload,我們可以提前載入資源,利用 media 屬性,瀏覽器只會載入需要的資源。

<link rel="preload" as="image" href="map.png" media="(max-width: 600px)">複製程式碼
<link rel="preload" as="script" href="map.js" media="(min-width: 601px)">複製程式碼


  • http header 實現預載入

Preload 還有一個特性是其可以通過 HTTP 頭資訊被呈現。也就是說上文中大多數的基於標記語言的宣告可以通過 HTTP 響應頭實現。(唯一的例外是有 onload 事件的例子,我們不可能在 HTTP 頭資訊中定義事件處理函式。)

Link: <thing_to_load.js>;rel="preload";as="script"複製程式碼
Link: <thing_to_load.woff2>;rel="preload";as="font";crossorigin複製程式碼

這一方式在有些場景尤其有用,比如,當負責優化的人員與頁面開發人員不是同一人時(也就是說優化人員可能無法或者不想修改頁面程式碼),還有一個傑出的例子是外部優化引擎(External optimization engine),該引擎對內容進行掃描並優化。


  • 瀏覽器特性檢查

前面所有的列子都基於一種假設——瀏覽器一定程度上支援 preload,至少實現了指令碼和樣式載入等基本功能。但如果這個假設不成立了。一切都將是然並卵。

為了判斷瀏覽器是否支援 preload,我們修改了 DOM 的規範從而能夠獲知 rel 支援那些值(是否支援 rel=‘preload’)。

至於如何進行檢查,原文中沒有,但 Github有一段程式碼可供參考。

var DOMTokenListSupports = function(tokenList, token) {複製程式碼
  if (!tokenList || !tokenList.supports) {複製程式碼
    return;複製程式碼
  }複製程式碼
  try {複製程式碼
    return tokenList.supports(token);複製程式碼
  } catch (e) {複製程式碼
    if (e instanceof TypeError) {複製程式碼
      console.log("The DOMTokenList doesn't have a supported tokens list");複製程式碼
    } else {複製程式碼
      console.error("That shouldn't have happened");複製程式碼
    }複製程式碼
  }複製程式碼
};複製程式碼
複製程式碼
var linkSupportsPreload = DOMTokenListSupports(document.createElement("link").relList, "preload");複製程式碼
if (!linkSupportsPreload) {複製程式碼
  // Dynamically load the things that relied on preload.複製程式碼
}複製程式碼

瀏覽器相容性:

caniuse.com 網站上顯示瀏覽器版本支援情況如下,目前還是比較高版本的瀏覽器會支援此功能,不過大家也不要擔心,在不支援的瀏覽器環境中,這部分標籤會被忽略,可以做到平穩降級。


相關文章