標籤:

CSS預載入Preload

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

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

<link rel=「preload」>

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

與現有的類似技術的區別

事實上,關於預載入,我們已經有<link rel=「prefetch」>,而且瀏覽器支持情況還不錯。

除此之外,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 是不支持的)。也就是說你可以定義資源載入完畢後的回調函數。

<link rel="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,你可以通過一段類似下面的代碼對瀏覽器說,」嗨,瀏覽器!這個資源你後面會用到,現在就載入它吧。「

<link rel="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 這個標準,簡單的一段代碼就能搞定字體的預載入。

<link rel="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);

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

先看代碼

<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 頭

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有一段代碼可供參考。

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 doesnt have a supported tokens list"); } else { console.error("That shouldnt 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上。

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

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

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

function LoadStyle(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> <meta charset="utf-8"> <title>Faster</title> <link rel="dns-prefetch" href="//cdn.cn/"> <link rel="preload" href="//cdn.cn/webfont.woff2" as="font"> <link rel="preload" href="//cdn.cn/Page1-A.js" as="script"> <link rel="preload" href="//cdn.cn/Page1-B.js" as="script"> <link rel="prefetch" href="//cdn.cn/Page2.js"> <link rel="prefetch" href="//cdn.cn/Page3.js"> <link rel="prefetch" href="//cdn.cn/Page4.js"> <style type="text/css"> /* 首頁用到的CSS內聯 */ </style></head><body><script type="text/javascript" src="//cdn.cn/Page1-A.js" defer></script><script type="text/javascript" src="//cdn.cn/Page1-B.js" defer></script></body></html>

推薦閱讀:

學了html和css,但是自己做靜態網頁布局的能力很差不知道怎麼提升?
CSS問題,這個搜索很難查到 .shadows-task的class後面怎麼跟著.task。還有「>"是什麼意思?
Razor 生成的html格式可以調整嗎?
(1 條消息)網站導航欄豎排並fix在窗體左側,但在屏幕寬度不夠需要橫向滾動時,導航欄會壓住其他元素,請問如何解決?
前端的清除浮動?

TAG:HTMLCSS |