使用localstorage和預載入做到webview秒開

積木村の研究所發表於2016-01-10

提到網頁載入速度優化,大家都會想到靜態資源上CDN,CSS和JS檔案合併,圖片合併成雪碧圖等常用手段;但是在某些特殊情況下這些常用方法也無法達到理想的效果。比如,在國際化場景下,很多國家還停留在2G網路階段,無論如何優化,都無法避免過慢的網路請求。最近一直在做國際化(主要是印尼和泰國)背景下的webview效能優化,也算有一些經驗。由於我們的產品是面向android使用者的,而android手機對H5支援很好,因此我們主要是應用H5的新特性。

1.H5的menifest快取的侷限

H5的menifest快取機制是大家經常提起的方法,這是一種使用起來很簡單的機制,其基本原理就是:當menifest檔案有更新時,就會更新整個應用,否則就不會請求網路而是使用本地快取的資源。但是我們使用起來卻有不少問題:比如即使menifest檔案發生變化,應用也不能及時的更新。對於menifest機制我們的結論是:menifest可以用於快取基本不發生變化的檔案(比如基本樣式表base.css,javascript類庫jquery.js等),對於業務程式碼一定不要使用menifest快取,否則可能導致業務程式碼無法更新

新浪微博中大量的使用了menifest快取機制,但是他們也只快取了類庫和基本樣式表,而沒有快取業務程式碼。這是一個例子 http://card.weibo.com/article/h5/s#cid=1001603928694585472076,其menifest檔案如下:

CACHE MANIFEST
# v = 8f708b371de7949de35b8cec4dac678b
CACHE:
# CSS and CSS resource image
http://u1.sinaimg.cn/apps/media/css/article_201511121505.css
http://u1.sinaimg.cn/apps/media/img/bg_cricle.png
http://u1.sinaimg.cn/apps/media/img/bg_criclein.png
http://u1.sinaimg.cn/apps/media/img/bg_imgslayer.png
http://u1.sinaimg.cn/apps/media/img/font/myfont_v3.eot
http://u1.sinaimg.cn/apps/media/img/font/myfont_v3.woff
http://u1.sinaimg.cn/apps/media/img/font/myfont_v3.ttf
http://u1.sinaimg.cn/apps/media/img/font/myfont_v3.svg
http://u1.sinaimg.cn/apps/media/img/lib/icons.svg
http://u1.sinaimg.cn/apps/media/img/lib/icon_SmartisanNote.svg
http://u1.sinaimg.cn/apps/media/img/lib/icon_Smart.svg

# Placeholder image
#http://ww3.sinaimg.cn/small/6f21f059jw1e4vxadm276j20f008ca9w.jpg

# js file
http://u1.sinaimg.cn/apps/media/js/jquery.min.js
http://u1.sinaimg.cn/apps/media/js/page/article/show/offline_201511121505.js

# Emoticons top 10
http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/6a/laugh.gif
http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/0b/tootha_org.gif
http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/6d/lovea_org.gif
http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/c4/liwu_org.gif
http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/9d/sada_org.gif
http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/40/hearta_org.gif
http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d9/ye_org.gif
http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/19/heia_org.gif
http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/8c/hsa_org.gif
http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d0/z2_org.gif

NETWORK:
*

2.使用localstorage快取js和css檔案

localstorage設計之初是為了快取應用資料的,關於是否應該使用它快取js和css檔案,知乎上有一篇討論: 靜態資源(JS/CSS)儲存在localStorage有什麼缺點?為什麼沒有被廣泛應用?。其中得票最多的基本上概況了localstorage的適用場景以及優缺點:

PC上因為localstorage相容性不好,而且網速較快,因此實用價值不大
移動端單頁面應用(webapp),因為localstorage相容性好,網速慢,所以值得嘗試
localstorage的安全性問題

UC的前端團隊推出了基於localstorage儲存前端靜態資源的Scrat-webapp模組化開發體系,而且基於這個模組化體系開發了很多專案:神馬視訊NBA直播。大家可以通過chrome的開發者工具檢視localstorage的使用情況。

基於以上的行業經驗,我們也選用了localstorage作為自己的前端資源快取,並實現了自己的基於localstorage的靜態資源載入器,其實現原理可以參考我的部落格:在網頁中非同步載入javascript 的XHR Eval方法。具體程式碼如下:

;(function (global) {
    'use strict';

    //檢查檔案型別
    var TYPE_RE = /\.(js|css)(?=[?&,]|$)/i;
    function fileType(str) {
        var ext = 'js';
        str.replace(TYPE_RE, function (m, $1) {
            ext = $1;
        });
        if (ext !== 'js' && ext !== 'css') ext = 'unknown';
        return ext;
    }

    //將js片段插入dom結構
    function evalGlobal(strScript){
        var scriptEl = document.createElement ("script");
        scriptEl.type= "text/javascript";
        scriptEl.text= strScript;
        document.getElementsByTagName("head")[0].appendChild(scriptEl) ;
    }

    //將css片段插入dom結構
    function createCss(strCss) {
        var styleEl = document.createElement('style');
        document.head.appendChild(styleEl);
        styleEl.appendChild(document.createTextNode(strCss));
    }

    // 在全域性作用域執行js或插入style node
    function defineCode(url, str) {
        var type = fileType(url);
        if (type === "js"){
            //with(window)eval(str);
            evalGlobal(str);
        }else if(type === "css"){
            createCss(str);
        }
    }

    // 將資料寫入localstorage
    var setLocalStorage = function(key, item){
        window.localStorage && window.localStorage.setItem(key, item);
    }

    // 從localstorage中讀取資料
    var getLocalStorage = function(key){
        return window.localStorage && window.localStorage.getItem(key);
    }

    // 通過AJAX請求讀取js和css檔案內容,使用佇列控制js的執行順序
    var rawQ = [];
    var monkeyLoader = {
        loadInjection: function(url,onload,bOrder){
            var iQ = rawQ.length;
            if(bOrder){
                var qScript = {key: null, response: null, onload: onload, done: false};
                rawQ[iQ] = qScript;
            }
            //有localstorage 快取
            var ls = getLocalStorage(url);
            if(ls !== null){
                if(bOrder){
                    rawQ[iQ].response = ls;
                    rawQ[iQ].key = url;
                    monkeyLoader.injectScripts();
                }else{
                    defineCode(url, ls)
                    if(onload){
                        onload();
                    }
                }
            } else {
                var xhrObj = monkeyLoader.getXHROject();
                xhrObj.open('GET', url, true);
                xhrObj.send(null);
                xhrObj.onreadystatechange = function(){
                    if(xhrObj.readyState == 4){
                        if(xhrObj.status == 200){
                            setLocalStorage(url, xhrObj.responseText);
                            if(bOrder){
                                rawQ[iQ].response = xhrObj.responseText;
                                rawQ[iQ].key = url;
                                monkeyLoader.injectScripts();
                            }else{
                                defineCode(url, xhrObj.responseText)
                                if(onload){
                                    onload();
                                }
                            }
                        }
                    }
                }
            }
        },

        injectScripts: function(){
            var len = rawQ.length;
            //按順序執行佇列中的指令碼
            for (var i = 0; i < len; i++) {
                var qScript = rawQ[i];
                //沒有執行
                if(!qScript.done){
                    //沒有載入完成
                    if(!qScript.response){
                        console.error("raw code lost or not load!");
                        //停止,等待載入完成, 由於指令碼是按順序新增到佇列的,因此這裡保證了指令碼的執行順序
                        break;
                    }else{//已經載入完成了
                        defineCode(qScript.key, qScript.response)
                        if(qScript.onload){
                            qScript.onload();
                        }
                        delete qScript.response;
                        qScript.done = true;
                    }
                }
            }
        },

        getXHROject: function(){
            //建立XMLHttpRequest物件
            var xmlhttp;
            if (window.XMLHttpRequest)
                {
                    // code for IE7+, Firefox, Chrome, Opera, Safari
                    xmlhttp=new XMLHttpRequest();
                } else {
                    // code for IE6, IE5
                    xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
                }
                return xmlhttp;
        }
    };


    global.monkeyLoader = monkeyLoader.loadInjection;

})(this);

在頁面中我們可以使用monkeyLoader載入自己的靜態資源

monkeyLoader('a.js',callbackA,true);
monkeyLoader('b.js',callbackB,true);

其中monkeyLoader的三個引數分別是:資原始檔路徑、資源載入完成後的回撥函式、以及資源是否需要有序。

3.基於fis3的localstorage整合解決方案

FIS3作為一個優秀的前端工程化解決方案,其在業界的影響無處不在。作為狼廠的一員,我們在專案中也大量使用FIS3,下面我們討論一下如何將localstorage整合到FIS3的編譯流程中。

我們的目的是將通過FIS3工程化處理後的檔案列表傳遞到monkeyLoader,以實現基於localstorage的載入。FIS3提供了靜態資源對映表,當某個檔案包含字元 RESOURCE_MAP,就會用表結構資料替換此字元,而該表記錄了各個原始靜態資源經過工程化後的目標檔案。基於此,我們只需要對monkeyLoader進行簡單的封裝即可。下面是具體的封裝程式碼:

fisMonkeyLoader = function(resourceMap,resourceList){
        var pkgList = [];
        for(var idx in resourceList){
            var resourceItem = resourceList[idx];
            var pakItem = resourceMap['pkg'][resourceMap['res'][resourceItem]['pkg']]['uri'];
            pkgList[pakItem] =  1;
        }
        for(var pkgIdx in pkgList){
            monkeyLoader(pkgIdx,null,true);
        }
}

而使用方法也很簡單:

__inline('../js/lib/monkeyLoader.js');
fisMonkeyLoader(__RESOURCE_MAP__,[
    'css/animate.css',
    'css/app.css',
    'js/lib/zepto-1.1.3.min.js',
    'js/app/app.js',
]);

fisMonkeyLoader根據RESOURCE_MAP找到原始資源對應的經過工程化處理以後的資源,並通過monkeyLoader進行載入。

也許你已經注意到了,使用localstorage只能載入同域的js和css檔案,因為我們沒辦法通過js程式碼讀取其他域下的靜態資源的內容。而且localstorage有大小限制,一般情況下每個域名下最多使用5M的本地儲存空間,因此我們務必要處理超出大小限制是的異常情況,各個瀏覽器丟擲的異常不太一致,這裡給出了一個跨瀏覽器的異常檢測方案

4.通過預載入解決首次載入過慢問題

針對目前常見的hybrid載入,我們可以通過讓native客戶端預載入webview從而提升首次速度。我們的實現是:客戶端在啟動時開啟一個1px大小(使用者不可見)的webview用於提前載入某些重要的頁面,而這些頁面通過上文中的localstorage機制儲存了幾乎全部js和css靜態資源,從而使得當使用者開啟webview時得到秒開的體驗。

本文同時發表在我的部落格積木村の研究所http://foio.github.io/localstorage-webview/

相關文章