所謂 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頁面不會)
- 讓chrome終端開啟的時候cache功能依舊有效
- 清空所有本地cache
- 開啟B頁面,在控制檯Networking裡看prefetching.html以及附屬的資原始檔是否被下載了
- 開啟A頁面,注意:是在位址列裡輸入A的網址然後回車,不要開啟A頁面後習慣性地按Command/Ctrl+R來重新整理,不出意外,我們會看到如下圖這樣的結果:
這說明,這2個css檔案是從cache裡讀的。如果Command/Ctrl+R來重新整理頁面,我們會看到這樣的結果: 兩者的差別是,Command/Ctrl+R的時候,瀏覽器會從cache裡找該靜態檔案,如果找到了,會根據上次請求這個檔案時得到的cache-control
資訊判斷該靜態檔案是否已經過期了,如果沒有,會以if-modified-since
、Etag
等資訊作為 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 files
點 Clear browsing data
檢視瀏覽器當前cache的資源列表:chrome://cache/
第二個版本,依樣畫葫蘆
目前看來,上面這個 File Prefeching 的方案是有效的。不過這種是最簡陋的試驗版,存在幾個問題:
- prefetching.html 裡的js會被執行,然後不可避免地會有一堆js錯誤 —— 看著難受~
- 通過iframe 載入 prefetching.html 會影響到當前頁面相關資源的載入速度
- 每次開啟頁面都會載入一次 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
- 其實還有一類資源可以加到這個prefetching.html裡,那就是常用的圖片,不過我們還沒這麼做。
- 現在我們有贊全部移動web頁只使用一個prefetching.html,並還沒有針對不同的條件進行鍼對性的的prefetching。
本文首發於有贊技術部落格:tech.youzan.com/file-frefet…