記錄從quicklink原始碼中發散出來的知識點
最近Google Chrome lab的一個開源專案quicklink很火,號稱可以極大提升頁面的載入速度,社群中也有很多使用該專案來做頁面載入優化的嘗試,但quicklink專案本身的實現其實極為簡潔,原始碼總共不過百行而已,其思路也不復雜,就是通過在瀏覽器空閒階段預載入view-port內的外部資源連結來提升頁面載入速度,總體來說並沒有什麼奇技淫巧。而我這裡主要想記錄和分享的是我在閱讀quicklink原始碼中頭腦發散的所思所得。
一,關於requestIdleCallback
首先我們來看下quicklink的原始碼結構
| src
| - index.mjs
| - prefetch.mjs
| - request-idle-callback.mjs
複製程式碼
index.mjs為專案的入口檔案,prefetch.mjs負責外部資源預載入的實現,request-idle-callback.mjs負責檢測瀏覽器是否處於空閒狀態,該模組的原始碼如下:
const requestIdleCallback = requestIdleCallback ||
function (cb) {
const start = Date.now();
return setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start));
},
});
}, 1);
};
export default requestIdleCallback;
複製程式碼
requestIdleCallback是Chrome瀏覽器 47版本之後提供的原生的效能優化API,用於在瀏覽器空閒時執行指定的回撥函式,這段程式碼是對requestIdleCallback的相容性實現,在一些沒有提供該API的瀏覽器中利用後面的自定義shim函式實現了對該功能的模擬。在閱讀這段程式碼時我的主要疑問是後面這段平平無奇利用setTimeout處理的函式為什麼就能達到requestIdleCallback的效果。於是乎我搜索了一下google關於web開發的博文,終於找到了
- requestIdleCallback接受一個回撥函式作為入參
- 該回調函式會接受一個名為deadline的object作為入參
- dealline包含一個timeRemaining的函式,該函式的返回值表示當前任務的剩餘時間,當該值為0時,就可以繼續規劃其他任務了。
- 當回撥被執行之後timeRemaining函式會返回0,deadline的didTimeout屬性值會為true
具體程式碼示例如下:
requestIdleCallback(myNonEssentialWork);
function myNonEssentialWork (deadline) {
while (deadline.timeRemaining() > 0 && tasks.length > 0)
doWorkIfNeeded();
if (tasks.length > 0)
requestIdleCallback(myNonEssentialWork);
}
複製程式碼
requestIdleCallback與requestAnimationFrame相同的是他們都接受一個回撥函式作為入參,回撥函式的執行時機由瀏覽器來決定,不同的是requestAnimationFrame在瀏覽器每幀繪製時都會執行,而requestIdleCallback必須是在瀏覽器執行緒空閒時才會執行,如果瀏覽器一直處於繁忙狀態,就有可能導致requestIdleCallback指定的任務一直都不會執行,要解決這個問題可以給requestIdleCallback傳遞一個包含timeout屬性的物件作為第二個入參,這樣瀏覽器在達到該timeout設定的時間後一定會執行回撥。示例程式碼如下:
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
複製程式碼
requestIdleCallback的機制使其很適合用來做一些對頁面來說不緊要的任務,比如說傳送頁面埋點資料之類。
二,關於prefetch與preload
首先來看下quicklink實現預載入的原始碼:
function linkPrefetchStrategy(url) {
return new Promise((resolve, reject) => {
const link = document.createElement(`link`);
link.rel = `prefetch`;
link.href = url;
link.onload = resolve;
link.onerror = reject;
document.head.appendChild(link);
});
};
function xhrPrefetchStrategy(url) {
return new Promise((resolve, reject) => {
const req = new XMLHttpRequest();
req.open(`GET`, url, req.withCredentials=true);
req.onload = () => {
(req.status === 200) ? resolve() : reject();
};
req.send();
});
}
function highPriFetchStrategy(url) {
return self.fetch == null
? xhrPrefetchStrategy(url)
: fetch(url, {credentials: `include`});
}
const supportedPrefetchStrategy = support('prefetch')
? linkPrefetchStrategy
: xhrPrefetchStrategy;
function prefetcher(url, isPriority, conn) {
if (preFetched[url]) {
return;
}
if (conn = navigator.connection) {
if ((conn.effectiveType || '').includes('2g') || conn.saveData) return;
}
return (isPriority ? highPriFetchStrategy : supportedPrefetchStrategy)(url).then(() => {
preFetched[url] = true;
});
};
export default prefetcher;
複製程式碼
這段程式碼的執行邏輯示意圖如下:
-
載入力度不同: preload會強制瀏覽器載入指定資源,同時不會阻塞document的onload事件;prefetch僅僅是提示瀏覽器這個資源將來可能需要,但至於是否要載入或者何時載入則由瀏覽器本身決定
-
應用場景不同: preload主要告知瀏覽器預先請求當前頁面所必要的資源;prefetch則主要是針對將來的頁面需要載入的資源。如果頁面A使用prefetch發起一個頁面B某個資源的請求,該請求與頁面B的導航請求有可能會同步進行,而如果使用preload,那麼當離開頁面A時preload請求會立即中斷。
-
網路優先順序不同: preload的優先順序要高於prefetch,而且preload不同as屬性的資源網路優先順序也不同,比如as='style'的優先順序就比as="script"的優先順序要高。
理解了這幾點差異之後之前的疑惑就迎刃而解了,在瀏覽器中,preload相對於fetch API,prefetch和xhr都具有更高的優先順序,且是強制載入,因此更適合用於處理高優先順序的資源請求,對於非優先順序的請求使用prefetch是比較合理的,相信quicklink未來也會更改函式highPriFetchStrategy的實現,用preload來完成。不管是prefetch還是preload,他們在完成資源載入後都不會執行,這一點也非常關鍵。
結語
quicklink是一個短小精悍的專案,雖然沒有什麼很高深的技術原理,但是仔細思考依然能發現一些比較有趣的小知識點,從點到面,不斷髮散才能更好的形成技術知識體系並活學活用,而不僅僅是一個知識點。比如requestIdleCallback的優化還可以怎麼用,react v16版本的一個重大變更就是fiber的任務排程演算法,在其中就利用了requestIdleCallback去執行低優先順序的任務。