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

AlienZHOU發表於2018-12-25

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

1. quicklink 是什麼?

quicklink 是一個通過預載入資源來提升後續速度的輕量級工具庫。旨在提升瀏覽過程中,使用者訪問後續頁面的載入速度。

當我們提到效能優化,往往都會著眼於當前使用者訪問的這個頁面,通過壓縮資源大小、刪減不必要資源、加快頁面解析渲染等方式提升使用者的訪問速度;而 quicklink 用了另一種思路:我預先幫你載入(獲取)接下來最可能要用的資源,這樣之後真正使用到該資源(連結)時就會感覺非常順暢。

照著這個思路,我們需要解決的問題就是如何預先幫使用者載入資源。這裡其實涉及到兩個問題:

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

下面就結合 quicklink 原始碼來看看如何解決這兩個問題。

注:下文提到的“預載入”/“預獲取”均指 prefetch

2. quicklink 實現原理

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

首先要解決的是,通過什麼方法來實現資源的預載入。即預載入的方式。

我們這裡的預載入對應的英文是 prefetch。提到 prefetch 自然會想到使用瀏覽器的 Resource Hints,通過提示瀏覽器做一些“預操作”(例如 DNS 解析、資源下載等)來加快後續的訪問。

如果對 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 與移動端是重災區。

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

美夢破滅。既然如此,我們就需要一個類似 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:實現原理與給前端的啟發

題外話: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 的核心。剩下的就是一些引數檢查、額外的規則特性等。

題外話:為了進一步保證效能,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 的實現,也能有自己的想法與啟發。

相信隨著瀏覽器的不斷進化,標準的不斷前行,前端工程師對極致體驗與效能要求的不斷提高,我們的產品將會越來越美好。

相關文章