【性能優化】quicklink:實現原理與給前端的啓發

近來,GoogleChromeLabs 推出了 quicklink,用以實現連接資源的預加載(prefetch)。本文在介紹其實現思路的基礎上,會進一步探討在預加載方面前端工程師還能夠作什麼。前端

1. quicklink 是什麼?

quicklink 是一個經過預加載資源來提高後續速度的輕量級工具庫。旨在提高瀏覽過程當中,用戶訪問後續頁面的加載速度。git

當咱們提到性能優化,每每都會着眼於當前用戶訪問的這個頁面,經過壓縮資源大小、刪減沒必要要資源、加快頁面解析渲染等方式提高用戶的訪問速度;而 quicklink 用了另外一種思路:我預先幫你加載(獲取)接下來最可能要用的資源,這樣以後真正使用到該資源(連接)時就會感受很是順暢。github

照着這個思路,咱們須要解決的問題就是如何預先幫用戶加載資源。這裏其實涉及到兩個問題:web

  • 如何去預加載一個指定資源?(預加載的方式)
  • 如何肯定某個資源是否要加載?(預加載的策略)

下面就結合 quicklink 源碼來看看如何解決這兩個問題。瀏覽器

注:下文提到的「預加載」/「預獲取」均指 prefetch緩存

2. quicklink 實現原理

2.1. 如何去預加載一個指定資源?

首先要解決的是,經過什麼方法來實現資源的預加載。即預加載的方式。性能優化

咱們這裏的預加載對應的英文是 prefetch。提到 prefetch 天然會想到使用瀏覽器的 Resource Hints,經過提示瀏覽器作一些「預操做」(例如 DNS 解析、資源下載等)來加快後續的訪問。bash

若是對 prefetch 與 Resource Hints 不熟悉,能夠看看這篇《使用Resource Hint提高頁面加載性能與體驗》網絡

只需下面一行代碼就能夠實現瀏覽器的資源預加載。是否是很是美妙?前端工程師

<link rel="prefetch" href="/my.little.script.js" as="script">
複製代碼

所以,對於一個指定的 URL,能夠經過下面四行代碼來預加載:

const link = document.createElement(`link`);
link.rel = `prefetch`;
link.href = url;
document.head.appendChild(link);
複製代碼

然而,咱們不得不面對兼容性的問題。Resource Hints 在低版本 IE 與移動端是重災區。

好夢破滅。既然如此,咱們就須要一個相似 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 中屬於中等優先級。

2.2. 如何肯定某個資源是否要預加載?

有了資源預加載的方式,那麼接下來就須要一個預加載的策略了。

這實際上是個見仁見智的問題。例如直接給你一個連接 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 的這篇文章

3. 到此爲止?不,咱們還能作更多

到這裏,quicklink 的實現就基本講完了。仔細回想一下,quicklink 其實提供了咱們一種經過「預加載」來實現性能優化的思路(粗略來講像是用流量換體驗)。這種方式我在前面也提到了,其實能夠分爲兩個部分:

  • 如何去預加載一個指定資源?(預加載的方式)
  • 如何肯定某個資源是否要加載?(預加載的策略)

其實兩部分彷佛都有能夠做爲的地方。例如如何保證 prefetcher(資源預加載器)的成功率能更高,以及目前使用的回退方案 XHR 其實在預加載沒法緩存的資源時所受的限制等。

此外,咱們在這裏還能夠來聊一聊策略這塊。

因爲 quicklink 是一個業務無關的輕量級功能庫,因此它採用了一個簡單但必定程度上有效的策略:預加載視野內的連接資源。然而在實際生產中,咱們面對的是更復雜的環境,更復雜的業務,反而會須要更精準的預加載判斷。所以,咱們徹底能夠從 quicklink 中剝離出 prefetcher 來做爲一個預加載器;而在策略部分使用本身的實現,例如:

  • 結合訪問日誌、打點記錄的更精準的預加載。例如,咱們能夠經過訪問日誌、打點記錄,根據 refer 來判斷,從 A 頁面來的 B、C、D 頁面的比例,從而設置一個閾值,超過該閾值則認爲訪問 A 頁面的用戶接下來更容易訪問它,從而對其預加載。

  • 結合用戶行爲數據來進行個性化的預加載。例如咱們有一個閱讀類或商品展現類站點,從用戶行爲發現,當該連接暴露在該用戶視野內 XX 秒(用戶閱讀內容 XX 秒)後點擊率達到 XX%。而不是簡單的一刀切或進入視野就預加載。

  • 後置非必要資源,精簡某類落地頁。落地頁就是要讓新用戶儘快「落地」,爲此咱們能夠像 Netflix 介紹的那樣,在宣貫頁/登陸頁精簡加載內容,而預加載後續主站的主包(主資源)。例若有些站點的首頁大多偏靜態,能夠用原生 JavaScript 加 內聯關鍵 CSS 的方式,加快加載,用戶訪問後再預加載 React、Vue 等一系列主站資源。

  • 等等。

上面這些場景只是拋磚引玉,相信你們還會有更多更好的場景來助力咱們的前端應用「起飛」。而咱們徹底能夠藉助一些構建工具、數據採集與分析平臺來實現策略的自動提取與注入,持續優化整個預加載的流程與效果。

寫在最後

預加載、Resource Hints 等由來已久。quicklink 經過提出一種可行的方案讓它又重回了你們的視野,給咱們展示了性能優化的另外一面。但願你們經過了解 quicklink 的實現,也能有本身的想法與啓發。

相信隨着瀏覽器的不斷進化,標準的不斷前行,前端工程師對極致體驗與性能要求的不斷提升,咱們的產品將會愈來愈美好。

相關文章
相關標籤/搜索