1. 程式人生 > >記錄從quicklink原始碼中發散出來的知識點

記錄從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;
複製程式碼

這段程式碼的執行邏輯示意圖如下:

qiucklink做預載入的思路是如果設定了優先順序屬性isPriority為true,就使用瀏覽器fetch API或者ajax的方式優先請求指定的URL資源,否則就判斷瀏覽器是否原生支援prefetch特性,如果支援就通過建立link標籤使用link tag的prefetch來載入資源。isPriority屬性預設為false,通常來說不會去改變它,所以對於非優先順序資源預載入的實現就主要依賴於link tag的prefetch特性,當我看到這裡的時候我很好奇為什麼是prefetch而不是preload,我們知道link標籤的preload屬性也可以做預載入,為什麼quicklink不使用preload呢?這兩者到底有什麼區別?因此我開始了深挖之旅,終於在medium上找到了相關資源, preload, prefetch and priority in chrome,medium訪問需要翻牆,且內容為英文,我查閱之後,概括為如下幾點:

  • 載入力度不同: 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去執行低優先順序的任務。