近來,GoogleChromeLabs 推出了 quicklink,用以實現連接資源的預加載(prefetch)。本文在介紹其實現思路的基礎上,會進一步探討在預加載方面前端工程師還能夠作什麼。前端
quicklink 是一個經過預加載資源來提高後續方案速度的輕量級工具庫。旨在提高瀏覽過程當中,用戶訪問後續頁面時的加載速度。git
當咱們提到性能優化,每每都會着眼於對當前用戶訪問的這個頁面,如何經過壓縮資源大小、刪減沒必要要資源、加快頁面解析渲染等方式提高用戶的訪問速度;而 quicklink 用了另外一種思路:我預先幫你加載(獲取)你接下來最可能要用的資源,這樣以後的真正使用到該資源(連接)時就會感受很是順暢。github
照着這個思路,咱們須要解決的問題就是如何預先幫用戶加載資源呢?這裏其實涉及到兩個問題:web
下面就結合 quicklink 源碼來看看如何解決這兩個問題。瀏覽器
注:下文提到的「預加載」/「預獲取」均指 prefetch
首先要解決的是,經過什麼方式來實現資源的預加載。即預加載的方式。緩存
咱們這裏的預加載對應的英文是 prefetch。提到 prefetch 天然會想到使用瀏覽器的 Resource Hints,經過提示瀏覽器作一些「預操做」(例如 DNS 解析、資源下載等)來加快後續的訪問。性能優化
若是對 prefetch 與 Resource Hints 不熟悉,能夠看看這篇 《使用Resource Hint提高頁面加載性能與體驗》。
只須要下面這樣一行代碼就能夠實現瀏覽器的資源預加載。是否是很是美妙?網絡
<link rel="prefetch" href="/my.little.script.js" as="script">
所以,要預加載一個資源能夠經過下面四行代碼:前端工程師
const link = document.createElement(`link`); link.rel = `prefetch`; link.href = url; document.head.appendChild(link);
然而,咱們不得不面對兼容性的問題,在低版本 IE 與移動端是重災區。app
好夢破滅。既然如此,咱們就須要一個相似 prefetch shim 的方式:在不支持 Resource Hints 的瀏覽器中,使用其餘方式來預加載資源。對此,咱們能夠利用瀏覽器自身的緩存策略,「實實在在」預先請求這個資源,這也造成了一種資源的「預獲取」。而這最方便的就是經過 XHR:
const req = new XMLHttpRequest(); req.open(`GET`, url, req.withCredentials=true); req.send();
這樣 shim 也完成了。最後,如何檢測瀏覽器是否支持 prefetch 呢?
咱們能夠經過 link
元素上 relList 屬性的 support
方法來檢查對 prefetch 的支持狀況:
const link = document.createElement('link'); link.relList || {}).supports && link.relList.supports('prefetch');
結合這三個段代碼,就造成了一個簡易的 prefetcher:判斷是否支持 Resource Hints 中的 prefetch,支持則使用它,不然回退使用 XHR 加載。
值得一提的是,使用 Resource Hints 與使用 XHR 來預加載資源仍是有一些重要差別的。草案中也提到了一些(主要是與性能以及與瀏覽器其餘行爲之間的衝突)。其中還有一點就是,Resource Hints 中的 prefetch 是否執行,徹底是由瀏覽器決定的,草案裏有句話很是明顯 —— the user agent SHOULD fetch。所以,全部 prefetch 的資源並不必定會真正被 prefetch。相較之下,XHR 的方式「成功率」則更高。這點在 Netflix 實施的性能優化案例中也提到了。
題外話:quicklink 中使用 fetch API 實現高優先級資源的加載。這是由於瀏覽器中會爲全部的請求都設置一個優先級,高優請求會被優先執行;目前,
fetch
在 Chrome 中屬於高優先級,在 Safari 中屬於中等優先級。
有了資源預加載的方式,那麼接下來就須要一個預加載的策略了。
這實際上是個見仁見智的問題。例如直接給你一個連接 https://my.test.com/somelink
,在沒有任何背景信息的狀況下,恐怕你徹底不知道是否須要預加載它。那對於這個問題,quicklink 是怎麼解決的呢?或者說,quicklink 是經過什麼策略來進行預加載的呢?
quicklink 用了一個比較直觀的策略:只對處於視口內的資源進行預加載。這一點也比較好理解,網絡上大多的資源加載、頁面跳轉都伴隨着用戶點擊這類行爲,而它要是不在你的視野內,你也就無從點擊了。這必定程度上算是個必要條件。
這麼一來,咱們所要解決的問題就是,若是判斷一個連接是否處於可視區域內?
之前,對於這種問題,咱們作的就是監聽 scroll
事件,而後判斷某元素的位置,從而來「得知」元素是否進入了視區。傳統的圖片懶加載庫 lazysize 等也是用這種策略。
document.addEventListener('scroll', function () { // ……判斷元素位置 });
注:目前 lazysize 也有了基於 IntersectionObserver 的實現
固然,須要特別注意滾動監聽的性能,例如使用截流、避免強制同步佈局、 passive: true
等方式緩解性能問題。
不過如今咱們有了一個新的方式來實現這一功能 —— IntersectionObserver
:
const observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { const link = entry.target; // 預加載連接 } }); }); // 對全部 a 標籤添加觀察者 Array.from(options.el.querySelectorAll('a'), link => { observer.observe(link); });
IntersectionObserver
會建立一個觀察者,專門用來觀察與通知元素進出視口的狀況。如上述代碼所示,IntersectionObserver
能夠觀察全部 a
元素的位置狀況(主要是進入視野)。
對IntersectionObserver
不瞭解的同窗能夠參考 Google 的IntersectionObserver
介紹文章。
可是以下圖所示, IntersectionObserver
存在兼容性問題,所以要在不兼容的瀏覽器中使用 quicklink,會須要一個 polyfill。
目前,咱們已經把 quicklink 的兩大部分(預加載的方式和預加載的策略)的原理和簡單實現講完了。整個 quicklink 很是簡潔,這些基本就是 quicklink 的核心。剩下的就是一些參數檢查、額外的規則特性等。
題外話:爲了進一步保證性能,quicklink 使用requestIdleCallback
在空閒時間查詢頁面a
標籤並掛載觀察者。對requestIdleCallback
不瞭解的同窗能夠看看 Google 的這篇文章。
到這裏,quicklink 的實現就基本講完了。仔細回想一下,quicklink 其實提供了咱們一種經過「預加載」來實現性能優化的思路(粗略來講像是用流量換體驗)。這種方式我在前面也提到了,其實能夠分爲兩個部分:
其實兩部分彷佛都有能夠做爲的地方。例如如何保證 prefetcher(資源預加載器)的成功率能更高,以及目前使用的回退方案 XHR 其實在預加載沒法緩存的資源時所受的限制等。
此外,咱們在這裏還能夠來聊一聊策略這塊。
因爲 quicklink 是一個業務無關的輕量級功能庫,因此它採用了一個簡單但必定程度上有效的策略:預加載視野內的連接資源。然而在實際生產中,咱們面對的是更復雜的環境,更復雜的業務,反而會須要更精準的預加載判斷。所以,咱們徹底能夠從 quicklink 中剝離出 prefetcher 來做爲一個預加載器;而在策略部分使用本身的實現,例如:
上面這些場景只是拋磚引玉,相信你們還會有更多更好的場景能夠來助力咱們的前端應用「起飛」。此外,咱們徹底能夠藉助一些構建工具、數據採集與分析平臺來實現策略的自動提取與注入,優化整個預加載的流程。
預加載、Resource Hints等由來已久。quicklink 經過提出了一種可行的方案讓它又進入了你們的視野,給咱們展示了性能優化的另外一面。但願你們經過了解 quicklink 的實現,也能有本身的想法與啓發。
相信隨着瀏覽器的不斷進化,標準的不斷前行,前端工程師對極致體驗與性能要求的不斷提升,咱們的產品將會愈來愈好。