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

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

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

一、建一個獨立的頁面,裏面索引了各類須要預加載的css、js,代碼相似下面這樣。php

<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>

二、 在每一個頁面加入一個iframe(通常經過base模板統一加),這樣每一個頁面打開的時候都會加載上面這個頁面。假設上面的頁面的url是 https://xxx.com/common/prefetching.html 那麼咱們每一個頁面底部都有這麼一行代碼:css

<iframe src="https://youzan.com/common/prefetching.html" sytle="display:none;"></iframe>

如何驗證

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

  1. 讓chrome終端打開的時候cache功能依舊有效
  2. 清空全部本地cache
  3. 打開B頁面,在控制檯Networking裏看prefetching.html以及附屬的資源文件是否被下載了
  4. 打開A頁面,注意:是在地址欄裏輸入A的網址而後回車,不要打開A頁面後習慣性地按Command/Ctrl+R來刷新,不出意外,咱們會看到以下圖這樣的結果:

圖片描述

這說明,這2個css文件是從cache裏讀的。若是Command/Ctrl+R來刷新頁面,咱們會看到這樣的結果: 前端

--------------------1.png

二者的差異是,Command/Ctrl+R的時候,瀏覽器會從cache裏找該靜態文件,若是找到了,會根據上次請求這個文件時獲得的cache-control信息判斷該靜態文件是否已通過期了,若是沒有,會以 if-modified-sinceEtag 等信息做爲 request headers 向服務器請求這個文件,服務器若是認爲文件沒有變過,會返回Http code爲304,瀏覽器因而直接讀cache。具體不展開啦,能夠看 《HTTP caching 》《Understanding HTTP/304 Responses》java

操做指引

_讓chrome終端打開的時候cache功能依舊有效_:Chrome終端的配置裏把Disable cache (while DevTools is open)的勾選去掉
_清空全部cache_:地址欄裏輸入 chrome://settings/clearBrowserData 打開後勾上 Cached images and filesClear browsing data
_查看瀏覽器當前cache的資源列表_:chrome://cache/web

第二個版本,依樣畫葫蘆

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

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

因而,咱們上線使用的版本是這樣的:segmentfault

一、有一段每一個頁面都會被執行到的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

二、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住了。若是是則正常輸出一個<link>標籤。若是不是,說明用戶是第一次訪問這個頁面,則直接把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。

本文首發於有贊技術博客:http://tech.youzan.com/file-f...

相關文章
相關標籤/搜索