所謂 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
》](https://developers.google.com/web/fundam... 和 《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住了。如果是則正常輸出一個<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
-
其實還有一類資源可以加到這個prefetching.html裏,那就是常用的圖片,不過我們還沒這麼做。
-
現在我們有贊全部移動web頁只使用一個prefetching.html,並還沒有針對不同的條件進行針對性的的prefetching。
本文首發於我的
SegmentFault專欄:https://segmentfault.com/a/1190000004189...
個人技術博客:http://delai.me/code/file-frefetching/
轉載請註明出處