預載入系列二:讓File Prefetching絲絲潤滑無痛無癢

有贊前端發表於2017-08-24

所謂 File Prefetching 就是在一個頁面載入成功後,默默去預載入後續可能會被訪問到的頁面的資源。
前端資源預載入其實沒啥新鮮的,我們倒騰這個事情的過程卻是很有有意思也很有啟發性。

第一個版本,簡單粗暴有點痛

1、建一個獨立的頁面,裡面索引了各種需要預載入的css、js,程式碼類似下面這樣。

<html>  
<head>  
    <link rel="stylesheet" href="//su.yzcdn.cn/v2/build_css/stylesheets/wap/showcase_d0fbaaef124a8691398704216ccd469a.css">
    ...其他需要預載入的css
</head>

<body>  
    <script src="//su.yzcdn.cn/v2/build/wap/common_08b03c7826.js" onerror="_cdnFallback(this)"></script>
    ...其他需要預載入的js
</body>  
</html>  複製程式碼

2、 在每個頁面加入一個iframe(一般通過base模板統一加),這樣每個頁面開啟的時候都會載入上面這個頁面。假設上面的頁面的url是 https://xxx.com/common/prefetching.html 那麼我們每個頁面底部都有這麼一行程式碼:

<iframe src="https://youzan.com/common/prefetching.html" sytle="display:none;"></iframe>  複製程式碼

如何驗證

要驗證某個file prefetching的方案是否真的有效,無非就是以下幾步: (假設A頁面使用了showcase_d0fbaaef124a8691398704216ccd469a.css,而B頁面不會)

  1. 讓chrome終端開啟的時候cache功能依舊有效
  2. 清空所有本地cache
  3. 開啟B頁面,在控制檯Networking裡看prefetching.html以及附屬的資原始檔是否被下載了
  4. 開啟A頁面,注意:是在位址列裡輸入A的網址然後回車,不要開啟A頁面後習慣性地按Command/Ctrl+R來重新整理,不出意外,我們會看到如下圖這樣的結果:
    這說明,這2個css檔案是從cache裡讀的。如果Command/Ctrl+R來重新整理頁面,我們會看到這樣的結果:
    兩者的差別是,Command/Ctrl+R的時候,瀏覽器會從cache裡找該靜態檔案,如果找到了,會根據上次請求這個檔案時得到的cache-control資訊判斷該靜態檔案是否已經過期了,如果沒有,會以 if-modified-sinceEtag 等資訊作為 request headers 向伺服器請求這個檔案,伺服器如果認為檔案沒有變過,會返回Http code為304,瀏覽器於是直接讀cache。具體不展開啦,可以看 《HTTP caching 》《Understanding HTTP/304 Responses》

操作指引

讓chrome終端開啟的時候cache功能依舊有效:Chrome終端的配置裡把Disable cache (while DevTools is open)的勾選去掉
清空所有cache:位址列裡輸入 chrome://settings/clearBrowserData 開啟後勾上 Cached images and filesClear browsing data
檢視瀏覽器當前cache的資源列表chrome://cache/

第二個版本,依樣畫葫蘆

目前看來,上面這個 File Prefeching 的方案是有效的。不過這種是最簡陋的試驗版,存在幾個問題:

  1. prefetching.html 裡的js會被執行,然後不可避免地會有一堆js錯誤 —— 看著難受~
  2. 通過iframe 載入 prefetching.html 會影響到當前頁面相關資源的載入速度
  3. 每次開啟頁面都會載入一次 prefetching.html,雖然裡面的靜態檔案都已經在第一次開啟的時候被cache住了不會重複下載,但無謂多一個請求終究是沒必要。

於是,我們上線使用的版本是這樣的:

1、有一段每個頁面都會被執行到的js:

// 開啟一個iframe,下載之後頁面可能需要的js/css
setTimeout(function() {  
    var lastOpenTime = 0;
    var nowTime = (new Date()).getTime();
    try {
        lastOpenTime = window.localStorage.getItem('staticIframeOpenTime');
    } catch (e) {}

    if (lastOpenTime > 0 && (nowTime - lastOpenTime < 24 * 3600 * 1000)) {
        // 24小時開啟一次iframe
        return;
    }

    var iframe = $('<iframe>').css('display', 'none');
    iframe
        .attr('src', 'https://youzan.com/common/prefetching.html')
        .appendTo(document.body);

    try {
        window.localStorage.setItem('staticIframeOpenTime', nowTime);
    } catch (e) {}
}, 3000);
// 延時3秒鐘載入prefetching.html複製程式碼

2、prefetching.html 裡的資源想辦法讓他下載但不執行,基本上都是把這些css/js檔案當做其他型別的檔案來載入,最後參照了《Preload CSS/JavaScript without execution》這篇文章,prefetching.html 中載入js檔案的程式碼大概是這樣的:

<script type="text/javascript">  
  window.onload = function () {
    var i = 0,
      max = 0,
      o = null,
      preload = [
        '需要預載入的檔案路徑'
      ],
      isIE = navigator.appName.indexOf('Microsoft') === 0;

    for (i = 0, max = preload.length; i < max; i += 1) {
      if (isIE) {
        new Image().src = preload[i];
        continue;
      }
      // firefox不相容 new Image().src 這種方式,所以除了IE都借用 object 來載入
      o = document.createElement('object');
      o.data = preload[i];

      o.width  = 0;
      o.height = 0;

      document.body.appendChild(o);
    }
  };
</script>  複製程式碼

通過對預載入的js檔案只下載不執行延時載入prefetching.html藉助localstorage的記錄一天只載入一次prefetching.html,基本上解決了版本一的3個問題。

效果和問題

移動頁面全站上線後,平均loaded時間減少了0.15s,首屏時間沒有資料,不過收益應該是可觀的

不過,這個版本上線後,我們發現頁面在prefetching的時候會假死,最後定位到是因為object載入js導致的(具體為什麼會這樣還沒細究),考慮到我們主要的頁面都是在手機端訪問的,基本上都是webkit核心(Image的方式在firefox中不相容也不甚關係),所以我們決定改用Image來載入所有JS。

第三個版本,完美

這個版本除了解決第二個版本的假死問題,還加入了dns-prefetch,關於這部分的背景和思路可以參考我另外一篇文章:《預載入系列一:DNS Prefetching 的正確使用姿勢》

<!DOCTYPE html>  
<html>  
<head>  
  <?php // dns prefething here ?>
  <link rel="dns-prefetch" href="//youzan.com/">
  ...

  <?php // css prefething here ?>
  <link rel="stylesheet" href="//su.yzcdn.cn/v2/build_css/stylesheets/wap/showcase_d0fbaaef124a8691398704216ccd469a.css">
  ...
</head>  
<body>  
  <?php // js prefething here ?>
  <script type="text/javascript">
    (function(){
      window.onload = function () {
        var i = 0,
          max = 0,
          preloadJs = [
            'js檔案路徑',
            ...
          ];

        for (i = 0, max = preloadJs.length; i < max; i += 1) {
          new Image().src = preloadJs[i];
        }
      };
    })();
  </script>
</body>  
</html>  複製程式碼

上線後,絲絲潤滑無痛無癢,完美

第四個版本,可以做更多

注意哦,重點來咯!
儘早載入css是減少首屏時間的關鍵(引申閱讀),直接把css inline到html裡是個不錯的方案。但是,這種方案的缺點是無法充分利用瀏覽器快取。所以,我們嘗試在現有的File Prefetching 的基礎上,再進一步,讓首次訪問足夠快(用css line),後續訪問又能利用起瀏覽器快取。

我們對一部分重點頁面的css檔案改用類似載入js的方式去載入,並在載入成功的回撥里加一條cookie記錄標示該css檔案已經被下載。這樣在後端輸出html的時候,可以根據cookie的資訊知道這幾個css檔案是不是已經在瀏覽器裡cache住了。如果是則正常輸出一個標籤。如果不是,說明使用者是第一次訪問這個頁面,則直接把css檔案的內容inline到html裡以求最快出首屏。當然,也會出現從cookie上看客戶端已經cache了某個css檔案,但實際上沒有的情況,由於這種情況下html裡輸出的還是一個link標籤,並不會影響正常的流程。

相關程式碼大概是這樣的,需要的朋友可以參考下:

var loadCss = function(key, url) {  
  var image = new Image();
  var date = new Date();

  date.setTime(+date + 1 * 86400000);
  // 因為下載的不是圖片,實際觸發的是onerror事件
  image.onload = image.onerror = function () {
    document.cookie = key + '=' + url.slice(url.indexOf('build_css')) + ';path=/;domain=.youzan.com;expires=' + date.toGMTString();
  };
  image.src = url;
}

preloadCss = {  
  key1: '檔案路徑',
  key2: '檔案路徑2'
  ...
}

for (var key in preloadCss) {  
  loadCss(key, preloadCss[key]);
}複製程式碼

總結

在做 File Prefetching 的過程當中,每一個版本的優化都是不同的人在做的:
A起了個頭 ->
B改進到能上線的標準 ->
發現有問題,C改進了它 ->
D又在這個基礎上做出了最後一個版本。

這種感覺非常好:)

TODO

  1. 其實還有一類資源可以加到這個prefetching.html裡,那就是常用的圖片,不過我們還沒這麼做。
  2. 現在我們有贊全部移動web頁只使用一個prefetching.html,並還沒有針對不同的條件進行鍼對性的的prefetching。

本文首發於有贊技術部落格:tech.youzan.com/file-frefet…

相關文章