1. 程式人生 > >【第1159期】CSS預載入Preload

【第1159期】CSS預載入Preload

前言

看天氣預報,今天好多地方都開始下雪了。今日早讀文章由@李斌分享。

正文從這開始~

Preload作為一個新的web標準,旨在提高效能和為web開發人員提供更細粒度的載入控制。Preload使開發者能夠自定義資源的載入邏輯,且無需忍受基於指令碼的資源載入器帶來的效能損失。

在 HTML 程式碼中,它看上去大概是下面這樣的一段宣告式獲取指令(declaratiev fetch directive)。

<linkrel=“preload”>

拿我們的話來說,通過這一方法我們告訴瀏覽器開始獲取某一特定資源,畢竟我們是作者,知道瀏覽器很快就會用到這一資源。

與現有的類似技術的區別

事實上,關於預載入,我們已經有<link rel=“prefetch”>

,而且瀏覽器支援情況還不錯。

640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1

除此之外,Chrome還支援過<link rel=“subresource”>

但Preload與這兩者不同,<link rel=“prefetch”>的作用是告訴瀏覽器載入下一頁面可能會用到的資源,注意,是下一頁面,而不是當前頁面。因此該方法的載入優先順序非常低(自然,相比當前頁面所需的資源,未來可能會用到的資源就沒那麼重要了),也就是說該方式的作用是加速下一個頁面的載入速度。

<link rel=“subresource”>的設計初衷是處理當前頁面,但最後還是壯烈犧牲了。因為開發者無法控制資源的載入優先順序,因此瀏覽器(其實也只有 Chrome 和基於 Chrome 的瀏覽器)在處理此類標籤時,優先順序很低,到底有多低呢?這麼說吧,在大多數情況下,用了等於沒用。

為什麼 Preload 更好

Preload是為處理當前頁面所生,這點和 subresource 一樣,但他們之間有著細微且意義重大的區別。Preload 有 as 屬性,這讓瀏覽器可做一些 subresource 和 prefetch 無法實現的事:

瀏覽器可以設定正確的資源載入優先順序,這種方式可以確保資源根據其重要性依次載入, 所以,Preload既不會影響重要資源的載入,又不會讓次要資源影響自身的載入。

瀏覽器可以確保請求是符合內容安全策略的,比如,如果我們的安全策略是Content-Security-Policy: script-src 'self',只允許瀏覽器執行自家伺服器的指令碼,as 值為 script 的外部伺服器資源就不會被載入。

瀏覽器能根據 as 的值傳送適當的 Accept 頭部資訊

瀏覽器通過 as 值能得知資源型別,因此當獲取的資源相同時,瀏覽器能夠判斷前面獲取的資源是否能重用。

Preload 的與眾不同還體現在 onload 事件上(至少在 Chrome 中,prefetch 和 subresource 是不支援的)。也就是說你可以定義資源載入完畢後的回撥函式。

<linkrel="preload"href="..."as="..."onload="preloadFinished()">

此外,preload 不會阻塞 windows 的 onload 事件,除非,preload資源的請求剛好來自於會阻塞 window 載入的資源。

結合上面所有這些特徵,preload 給我們帶來了一些以前不可能實現的功能。

資源的提前載入:

preload 一個基本的用法是提前載入資源,儘管大多數基於標記語言的資源能被瀏覽器的預載入器(Preloader)儘早發現,但不是所有的資源都是基於標記語言的,比如一些隱藏在 CSS 和 Javascript 中的資源。當瀏覽器發現自己需要這些資源時已經為時已晚,所以大多數情況,這些資源的載入都會對頁面渲染造成延遲。

Preloader 簡介

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

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

現在,有了 preload,你可以通過一段類似下面的程式碼對瀏覽器說,”嗨,瀏覽器!這個資源你後面會用到,現在就載入它吧。“

<linkrel="preload"href="late_discovered_thing.js"as="script">

as 屬性的作用是告訴瀏覽器被載入的是什麼資源,可能的 as 值包括:

  • “script”

  • “style”

  • “image”

  • “media”

  • “document”

更多請參考fetch spec

忽略 as 屬性,或者錯誤的 as 屬性會使 preload 等同於 XHR 請求,瀏覽器不知道載入的是什麼,因此會賦予此類資源非常低的載入優先順序。

對字型的提前載入

web 字型是較晚才能被發現的關鍵資源(late-discovered critical resources)中常見的一類 。web 字型對頁面文字的渲染資至關重要,但卻被深埋 CSS 中,即便是預載入器有解析 CSS,也無法確定包含字型資訊的選擇器是否會真正應用在 DOM 節點上。理論上,這個問題可以被解決,但實際情況是沒有一個瀏覽器解決了這個問題。而且,即便是問題得到了解決,瀏覽器能對字型檔案做出合理的預載入,一旦有新的 css 規則覆蓋了現有字型規則,前面的預載入就多餘了。

總之,非常複雜。

但有了 preload 這個標準,簡單的一段程式碼就能搞定字型的預載入。

<linkrel="preload"href="font.woff2"as="font"type="font/woff2"crossorigin>

需要注意的一點是:crossorigin 屬性是必須的,即便是字型資源在自家伺服器上,因為使用者代理必須採用匿名模式來獲取字型資源。

type 屬性可以確保瀏覽器只獲取自己支援的資源。儘管Chrome 支援 WOFF2,也是目前唯一支援 preload 的瀏覽器,但未來或許會有更多的瀏覽器支援 preload,而這些瀏覽器支不支援 WOFF2 就不好說了。

動態載入,但不執行

另外一個有意思的場景也因為 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);

基於標記語言的非同步載入

先看程式碼

<linkrel="preload"as="style"href="asyncstyle.css"onload="this.rel='stylesheet'">

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

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

難道我們不是已經有了

<script async>

雖好,但卻會阻塞 window 的 onload 事件。某些情況下,你可能希望這樣,但總有一些情況你不希望阻塞 window 的 onload 。

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

有了preload, 分分鐘搞定。

<linkrel="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 屬性,瀏覽器只會載入需要的資源。

<linkrel="preload"as="image"href="map.png"media="(max-width: 600px)"><linkrel="preload"as="script"href="map.js"media="(min-width: 601px)">

HTTP 頭

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),該引擎對內容進行掃描並優化。

特徵檢查 (Feature Detection)

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

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

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

varDOMTokenListSupports=function(tokenList, token){if(!tokenList ||!tokenList.supports){return;}try{return tokenList.supports(token);}catch(e){if(e instanceofTypeError){
     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.}

討論地址:https://github.com/w3c/preload/issues/7

是否可以用 HTTP/2 Push 完成 preload 的工作?

當然不行,儘管有一些相同的特性,但總的來說,他們的關係是互補而不是取代。

HTTP/2 Push 的優勢是能夠主動推送資源給瀏覽器,也就是說,伺服器甚至不需要等到資源請求就能將資源推送給瀏覽器。

而 Preload 的優勢在於其載入過程是透明的,一旦資源載入完畢或出現異常,應用可以獲得事件通知。這一點是 HTTP/2 Push所不具備的。另外,Preload 還能載入第三方資源,但 HTTP/2 Push 不能。

此外,HTTP/2 Push 沒辦法將瀏覽器的快取和非全域性 cookie (non-global cookie) 考慮進去。也就是說,伺服器推送的內容可能已經存在於客戶端的快取中,從而導致毫無意義的網路傳輸。(不過一份新的規範旨在解決該問題——cache digest specification,Github 上的 一個輕量級 Web服務H2O器實現了該功能,H2O在1.5版中引入了基於cookie 的cache-aware server push,原理是在首次 Server Push 完成後,在客戶端存一個指紋,服務端後續檢查到指紋存在時,先在指紋中查詢要 Push 的資源,沒查到才推送),但是非全域性的 cookie就沒這麼好運了。對於這型別的資源,Preload 才是你的朋友。

Preload還有一個HTTP/2 Push 所不具備的能力是可以進行內容協商(content negotiation),也就是說如果你想通過Client-Hints或者 HTTP 頭的 accept 資訊獲取最合適的資源格式,HTTP/2 Push 幫不了你。

優化核心依舊是減少下載時間

JS篇中的預先解析DNS(dns-prefetch)依舊適用,提前解析CSS檔案所在域名的DNS。

Preload

因為CSS已經在head中,我們不需要為css加preload屬性了,但是css中用到的字型檔案,一定要在所有css之前proload上。

<linkrel="preload"href="/webfont.woff2"as="font">

首頁CSS內聯,非必要CSS非同步載入

首頁用到的CSS內聯寫在<head>中,其餘CSS均採用非同步載入,可以採用這種自己實現的載入CSS的方法,在合適的需要時載入需要的css

functionLoadStyle(url){try{
   document
.createStyleSheet(url)}catch(e){var cssLink = document.createElement('link');
   cssLink
.rel ='stylesheet';
   cssLink
.type ='text/css';
   cssLink
.href = url;var head = document.getElementsByTagName('head')[0];
   head
.appendChild(cssLink)}}

如果你使用webpack,那就更輕鬆了,使用import函式,大致如下

// 在a.js模組中直接引入cssimport'style.css'// 在需要a.js模組的地方
improt
('path-of-a.js').then(module=>{})

webpack打包後,其實是把style.css打包進了a.js,在非同步載入a.js的時候,會將style.css中的程式碼插入haed標籤中。

終極完美結構

<!DOCTYPE html><html><head><metacharset="utf-8"><title>Faster</title><linkrel="dns-prefetch"href="//cdn.cn/"><linkrel="preload"href="//cdn.cn/webfont.woff2"as="font"><linkrel="preload"href="//cdn.cn/Page1-A.js"as="script"><linkrel="preload"href="//cdn.cn/Page1-B.js"as="script"><linkrel="prefetch"href="//cdn.cn/Page2.js"><linkrel="prefetch"href="//cdn.cn/Page3.js"><linkrel="prefetch"href="//cdn.cn/Page4.js"><styletype="text/css">/* 首頁用到的CSS內聯 */</style></head><body><scripttype="text/javascript"src="//cdn.cn/Page1-A.js"defer></script><scripttype="text/javascript"src="//cdn.cn/Page1-B.js"defer></script></body></html>

最後,為你推薦

關於本文
作者:@李斌
原文:https://zhuanlan.zhihu.com/p/32561606

0?wx_fmt=jpeg