近來,GoogleChromeLabs 推出了 quicklink,用以實現(xiàn)鏈接資源的預(yù)加載(prefetch)。本文在介紹其實現(xiàn)思路的基礎(chǔ)上,會進一步探討在預(yù)加載方面前端工程師還可以做什么。
1. quicklink 是什么的?
quicklink 是一個通過預(yù)加載資源來提升后續(xù)方案速度的輕量級工具庫。旨在提升瀏覽過程中,用戶訪問后續(xù)頁面時的加載速度。
當(dāng)我們提到性能優(yōu)化,往往都會著眼于對當(dāng)前用戶訪問的這個頁面,如何通過壓縮資源大小、刪減不必要資源、加快頁面解析渲染等方式提升用戶的訪問速度;而 quicklink 用了另一種思路:我預(yù)先幫你加載(獲?。┠憬酉聛碜羁赡芤玫馁Y源,這樣之后的真正使用到該資源(鏈接)時就會感覺非常順暢。
照著這個思路,我們需要解決的問題就是如何預(yù)先幫用戶加載資源呢?這里其實涉及到兩個問題:
- 如何去預(yù)加載一個指定資源?(預(yù)加載的方式)
- 如何確定某個資源是否要加載?(預(yù)加載的策略)
下面就結(jié)合 quicklink 源碼來看看如何解決這兩個問題。
注:下文提到的“預(yù)加載”/“預(yù)獲取”均指 prefetch
2. quicklink 實現(xiàn)原理
2.1. 如何去預(yù)加載一個指定資源?
首先要解決的是,通過什么方式來實現(xiàn)資源的預(yù)加載。即預(yù)加載的方式。
我們這里的預(yù)加載對應(yīng)的英文是 prefetch。提到 prefetch 自然會想到使用瀏覽器的 Resource Hints,通過提示瀏覽器做一些“預(yù)操作”(例如 DNS 解析、資源下載等)來加快后續(xù)的訪問。
如果對 prefetch 與 Resource Hints 不熟悉,可以看看這篇《使用Resource Hint提升頁面加載性能與體驗》。
只需要下面這樣一行代碼就可以實現(xiàn)瀏覽器的資源預(yù)加載。是不是非常美妙?
<link rel="prefetch" href="/my.little.script.js" as="script">
因此,要預(yù)加載一個資源可以通過下面四行代碼:
const link = document.createElement(`link`);
link.rel = `prefetch`;
link.href = url;
document.head.appendChild(link);
然而,我們不得不面對兼容性的問題,在低版本 IE 與移動端是重災(zāi)區(qū)。
美夢破滅。既然如此,我們就需要一個類似 prefetch shim 的方式:在不支持 Resource Hints 的瀏覽器中,使用其他方式來預(yù)加載資源。對此,我們可以利用瀏覽器自身的緩存策略,“實實在在”預(yù)先請求這個資源,這也形成了一種資源的“預(yù)獲取”。而這最方便的就是通過 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');
結(jié)合這三個段代碼,就形成了一個簡易的 prefetcher:判斷是否支持 Resource Hints 中的 prefetch,支持則使用它,否則回退使用 XHR 加載。
值得一提的是,使用 Resource Hints 與使用 XHR 來預(yù)加載資源還是有一些重要差異的。草案中也提到了一些(主要是與性能以及與瀏覽器其他行為之間的沖突)。其中還有一點就是,Resource Hints 中的 prefetch 是否執(zhí)行,完全是由瀏覽器決定的,草案里有句話非常明顯 —— the user agent SHOULD fetch。因此,所有 prefetch 的資源并不一定會真正被 prefetch。相較之下,XHR 的方式“成功率”則更高。這點在 Netflix 實施的性能優(yōu)化案例中也提到了。
題外話:quicklink 中使用 fetch API 實現(xiàn)高優(yōu)先級資源的加載。這是因為瀏覽器中會為所有的請求都設(shè)置一個優(yōu)先級,高優(yōu)請求會被優(yōu)先執(zhí)行;目前,
fetch在 Chrome 中屬于高優(yōu)先級,在 Safari 中屬于中等優(yōu)先級。
2.2. 如何確定某個資源是否要預(yù)加載?
有了資源預(yù)加載的方式,那么接下來就需要一個預(yù)加載的策略了。
這其實是個見仁見智的問題。例如直接給你一個鏈接 https://my.test.com/somelink,在沒有任何背景信息的情況下,恐怕你完全不知道是否需要預(yù)加載它。那對于這個問題,quicklink 是怎么解決的呢?或者說,quicklink 是通過什么策略來進行預(yù)加載的呢?
quicklink 用了一個比較直觀的策略:只對處于視口內(nèi)的資源進行預(yù)加載。這一點也比較好理解,網(wǎng)絡(luò)上大多的資源加載、頁面跳轉(zhuǎn)都伴隨著用戶點擊這類行為,而它要是不在你的視野內(nèi),你也就無從點擊了。這一定程度上算是個必要條件。
這么一來,我們所要解決的問題就是,如果判斷一個鏈接是否處于可視區(qū)域內(nèi)?
以前,對于這種問題,我們做的就是監(jiān)聽 scroll 事件,然后判斷某元素的位置,從而來“得知”元素是否進入了視區(qū)。傳統(tǒng)的圖片懶加載庫 lazysize 等也是用這種策略。
document.addEventListener('scroll', function () {
// ……判斷元素位置
});
注:目前 lazysize 也有了基于 IntersectionObserver 的實現(xiàn)
當(dāng)然,需要特別注意滾動監(jiān)聽的性能,例如使用截流、避免強制同步布局、 passive: true 等方式緩解性能問題。
不過現(xiàn)在我們有了一個新的方式來實現(xiàn)這一功能 —— IntersectionObserver:
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const link = entry.target;
// 預(yù)加載鏈接
}
});
});
// 對所有 a 標(biāo)簽添加觀察者
Array.from(options.el.querySelectorAll('a'), link => {
observer.observe(link);
});
IntersectionObserver 會創(chuàng)建一個觀察者,專門用來觀察與通知元素進出視口的情況。如上述代碼所示,IntersectionObserver 可以觀察所有 a 元素的位置情況(主要是進入視野)。
對
IntersectionObserver不了解的同學(xué)可以參考 Google 的IntersectionObserver介紹文章。
但是如下圖所示, IntersectionObserver 存在兼容性問題,因此要在不兼容的瀏覽器中使用 quicklink,會需要一個 polyfill。
目前,我們已經(jīng)把 quicklink 的兩大部分(預(yù)加載的方式和預(yù)加載的策略)的原理和簡單實現(xiàn)講完了。整個 quicklink 非常簡潔,這些基本就是 quicklink 的核心。剩下的就是一些參數(shù)檢查、額外的規(guī)則特性等。
題外話:為了進一步保證性能,quicklink 使用
requestIdleCallback在空閑時間查詢頁面a標(biāo)簽并掛載觀察者。對requestIdleCallback不了解的同學(xué)可以看看 Google 的這篇文章。
3. 到此為止?不,我們還能做更多
到這里,quicklink 的實現(xiàn)就基本講完了。仔細回想一下,quicklink 其實提供了我們一種通過“預(yù)加載”來實現(xiàn)性能優(yōu)化的思路(粗略來說像是用流量換體驗)。這種方式我在前面也提到了,其實可以分為兩個部分:
- 如何去預(yù)加載一個指定資源?(預(yù)加載的方式)
- 如何確定某個資源是否要加載?(預(yù)加載的策略)
其實兩部分似乎都有可以作為的地方。例如如何保證 prefetcher(資源預(yù)加載器)的成功率能更高,以及目前使用的回退方案 XHR 其實在預(yù)加載無法緩存的資源時所受的限制等。
此外,我們在這里還可以來聊一聊策略這塊。
由于 quicklink 是一個業(yè)務(wù)無關(guān)的輕量級功能庫,所以它采用了一個簡單但一定程度上有效的策略:預(yù)加載視野內(nèi)的鏈接資源。然而在實際生產(chǎn)中,我們面對的是更復(fù)雜的環(huán)境,更復(fù)雜的業(yè)務(wù),反而會需要更精準(zhǔn)的預(yù)加載判斷。因此,我們完全可以從 quicklink 中剝離出 prefetcher 來作為一個預(yù)加載器;而在策略部分使用自己的實現(xiàn),例如:
結(jié)合訪問日志、打點記錄的更精準(zhǔn)的預(yù)加載。例如,我們可以通過訪問日志、打點記錄,根據(jù) refer 來判斷,從 A 頁面來的 B、C、D 頁面的比例,從而設(shè)置一個閾值,超過該閾值則認(rèn)為訪問 A 頁面的用戶接下來更容易訪問它,從而對其預(yù)加載。
結(jié)合用戶行為數(shù)據(jù)來進行個性化的預(yù)加載。例如我們有一個閱讀類或商品展示類站點,從用戶行為發(fā)現(xiàn),當(dāng)該鏈接暴露在該用戶視野內(nèi) XX 秒(用戶閱讀內(nèi)容 XX 秒)后點擊率達到 XX%。而不是簡單的一刀切或進入視野就預(yù)加載。
后置非必要資源,精簡某類落地頁。落地頁就是要讓新用戶盡快“落地”,為此我們可以像 Netflix 介紹的那樣,在宣貫頁/登錄頁精簡加載內(nèi)容,而預(yù)加載后續(xù)主站的主包(主資源)。例如有些站點的首頁大多偏靜態(tài),可以用原生 JavaScript 加 內(nèi)聯(lián)關(guān)鍵 CSS 的方式,加快加載,用戶訪問后再預(yù)加載 React、Vue 等一系列主站資源。
等等。
上面這些場景只是拋磚引玉,相信大家還會有更多更好的場景可以來助力我們的前端應(yīng)用“起飛”。此外,我們完全可以借助一些構(gòu)建工具、數(shù)據(jù)采集與分析平臺來實現(xiàn)策略的自動提取與注入,優(yōu)化整個預(yù)加載的流程。
寫在最后
預(yù)加載、Resource Hints等由來已久。quicklink 通過提出了一種可行的方案讓它又進入了大家的視野,給我們展現(xiàn)了性能優(yōu)化的另一面。希望大家通過了解 quicklink 的實現(xiàn),也能有自己的想法與啟發(fā)。
相信隨著瀏覽器的不斷進化,標(biāo)準(zhǔn)的不斷前行,前端工程師對極致體驗與性能要求的不斷提高,我們的產(chǎn)品將會越來越好。