1. 程式人生 > >HTML優化之按需載入

HTML優化之按需載入

按需載入是前端效能優化中的一項重要措施,按需載入是如何定義的呢?顧名思義,指的是當用戶觸發了動作時才載入對應的功能。觸發的動作,是要看具體的業務場景而言,包括但不限於以下幾個情況:滑鼠點選、輸入文字、拉動滾動條,滑鼠移動、視窗大小更改等。載入的檔案,可以是JS、圖片、CSS、HTML等。後面將會詳細介紹“按需”的理解。
按需解析HTML
按需解析HTML,就是頁面一開始不解析HTML,根據需要來解析HTML。解析HTML都是需要一定時間,特別是HTML中包含有img標籤、引用了背景圖片時,如果一開始就解析,那麼勢必會增加請求數。常見的有對話方塊、拉選單、多標籤的內容展示等,這些一開始是不需要解析,可以按需解析。實現按需解析,首先用

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>按需解析HTML</title>
</head>
<body>
<script type="text/x-template" id="suc_subscription">
    <!--假設這裡的樣式box-dytz 引用了一張背景圖--->
    <div>
<!--這裡暫且用這張圖片作為測試,實際中,大家可以替換為任何圖片--> <img src="http://tid.tenpay.com/wp-content/uploads/2012/12/按需載入.jpg" /> </div>
</script> <div id="success_dilog"></div> <input type="button" value="點我展示HTML" onclick="showHTML()" /> <script> function showHTML()
{
document.getElementById('success_dilog').innerHTML = document.getElementById('suc_subscription').innerHTML; }
</script> </body> </html>

我們一起來看下demo,當執行demo並抓包發現:當頁面載入結束時,並沒有看到圖片的請求;當點“點我展示HTML”按鈕時,通過抓包發現有圖片請求。
曾經做個demo並經過測試發現,如果是直接解析HTML(不包含有請求CSS圖片和img標籤),耗費的時間要比用大約慢1-2倍,如果是還包括請求有CSS圖片、img標籤,請求連線數將會更多,可見按需解析HTML,對效能提升還是有一定效果。
按需載入圖片
按需載入圖片,就是讓圖片預設開始不載入,而且在接近可視區域範圍時,再進行載入。也稱之為懶惰載入。大家都知道,圖片一下子全部都載入,請求的次數將會增加,勢必影響效能。
先來看下懶惰載入的實現原理。它的觸發動作是:當滾動條拉動到某個位置時,即將進入可視範圍的圖片需要載入。實現的過程分為下面幾個步驟:
生成標籤時,用data-src來儲存圖片地址;
記錄的圖片data-src都儲存到數組裡;
對滾動條進行事件繫結,假設繫結的函式為function lazyload(){};
在函式lazyload中,按照下面思路實現:計算圖片的Y座標,並計算可視區域的高度height,當Y小於等於(height+ scrollTop)時,圖片的src的值用data-src的來替換,從而來實現圖片的按需載入;
下面看一個示例程式碼:

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>圖片按需載入</title>
</head>
<body>
    <style>
        li {float:left;width:200px;}
    </style>
    <div style="widh:100%;height:1200px;border:1px solid #000">這裡是空白的內容,高度為900畫素,目的是方便出現滾動條</div>
    <ul style="width:600px;">
        <li> <img width="158" height="158" data-src="http://pic6.nipic.com/20100423/4537140_133035029164_2.jpg"  /> </li>
        <li> <img width="158" height="158" data-src="http://www.jiujiuba.com/xxpict2/picnews/62223245.jpg"  /> </li>
        <li> <img width="158" height="158" data-src="http://www.bz55.com/uploads/allimg/100729/095KS455-0.jpg"  /> </li>
        <li> <img width="158" height="158" data-src="http://www.hyrc.cn/upfile/3/200611/1123539053c7e.jpg"/> </li>
        <li> <img width="158" height="158" data-src="http://www.mjjq.com/blog/photos/Image/mjjq-photos-903.jpg" /> </li>
        <li> <img width="158" height="158" data-src="http://c.cncnimg.cn/000/954/1416_2_b.jpg"  /> </li>
        <li> <img width="158" height="158" data-src="http://www.jiujiuba.com/xxpict2/picnews/62223231.jpg" /> </li>
        <li> <img width="158" height="158" data-src="http://www.mjjq.com/pic/20070530/20070530043314558.jpg" /> </li>
    </ul>
    <script>
        var API = {   
            /**
              * 相容Mozilla(attachEvent)和IE(addEventListener)的on事件
              * @param {String} element DOM物件 例如:window,li等
              * @param {String} type on事件型別,例如:onclick,onscroll等
              * @param {Function} handler 回撥事件
              */
            on: function(element, type, handler) {
                return element.addEventListener ? element.addEventListener(type, handler, false) : element.attachEvent('on' + type, handler)
            },   
            /**
              * 為物件繫結事件
              * @param {Object} object 物件
              * @param {Function} handler 回撥事件
              */
            bind: function(object, handler) {
                return function() {
                    return handler.apply(object, arguments)
                }
            },   
            /**
              * 元素在頁面中X軸的位置
              * @param {String} element DOM物件 例如:window,li等
              */
            pageX: function(El) {
                var left = 0;
                 do {
                    left += El.offsetLeft;

                } while(El.offsetParent && (El = El.offsetParent).nodeName.toUpperCase() != 'BODY');
                return left;

            },   
            /**
              * 元素在頁面中Y軸的位置
              * @param {String} element DOM物件 例如:window,li等
              */
            pageY: function(El) {
                var top = 0;
                 do {
                    top += El.offsetTop;

                } while(El.offsetParent && (El = El.offsetParent).nodeName.toUpperCase() != 'BODY');
                return top;

            },   
            /**
              * 判斷圖片是否已載入
              * @param {String} element DOM物件 例如:window,li等
              * @param {String} className 樣式名稱
              * @return {Boolean} 布林值
              */
            hasClass: function(element, className) {
                return new RegExp('(^|\\s)' + className + '(\\s|$)').test(element.className)
            },   
            /**
              * 獲取或者設定當前元素的屬性值
              * @param {String} element DOM物件 例如:window,li等
              * @param {String} attr 屬性
              * @param {String} (value) 屬性的值,此引數如果沒有那麼就是獲取屬性值,此引數如果存在那麼就是設定屬性值
              */
            attr: function(element, attr, value) {
                 if (arguments.length == 2) {
                    return element.attributes[attr] ? element.attributes[attr].nodeValue : undefined
                }
                else if (arguments.length == 3) {
                    element.setAttribute(attr, value)
                }
            }
        };

        /**
          * 按需載入
          * @param {String} obj 圖片區域元素ID
          */
        function lazyload(obj) {
            this.lazy = typeof obj === 'string' ? document.getElementById(obj) : document.getElementsByTagName('body')[0];
            this.aImg = this.lazy.getElementsByTagName('img');
            this.fnLoad = API.bind(this, this.load);
            this.load();
            API.on(window, 'scroll', this.fnLoad);
            API.on(window, 'resize', this.fnLoad);

        }
        lazyload.prototype = {

            /**
              * 執行按需載入圖片,並將已載入的圖片標記為已載入
              * @return 無
              */
            load: function() {
                var iScrollTop = document.documentElement.scrollTop || document.body.scrollTop;
                // 螢幕上邊緣
                var iClientHeight = document.documentElement.clientHeight + iScrollTop;
                // 螢幕下邊緣
                var i = 0;
                var aParent = [];
                var oParent = null;
                var iTop = 0;
                var iBottom = 0;
                var aNotLoaded = this.loaded(0);
                 if (this.loaded(1).length != this.aImg.length) {
                    var notLoadedLen = aNotLoaded.length;
                     for (i = 0; i < notLoadedLen; i++) {
                        iTop = API.pageY(aNotLoaded[i]) - 200;
                        iBottom = API.pageY(aNotLoaded[i]) + aNotLoaded[i].offsetHeight + 200;
                        var isTopArea = (iTop > iScrollTop && iTop < iClientHeight) ? true : false;
                        var isBottomArea = (iBottom > iScrollTop && iBottom < iClientHeight) ? true : false;
                         if (isTopArea || isBottomArea) {                   
                            // 把預存在自定義屬性中的真實圖片地址賦給src
                            aNotLoaded[i].src = API.attr(aNotLoaded[i], 'data-src') || aNotLoaded[i].src;
                             if (!API.hasClass(aNotLoaded[i], 'loaded')) {
                                 if ('' != aNotLoaded[i].className) {
                                    aNotLoaded[i].className = aNotLoaded[i].className.concat(" loaded");

                                }
                                else {
                                    aNotLoaded[i].className = 'loaded';

                                }
                            }
                        }
                    }
                }
            },

            /**
              * 已載入或者未載入的圖片陣列
              * @param {Number} status 圖片是否已載入的狀態,0代表未載入,1代表已載入
              * @return Array 返回已載入或者未載入的圖片陣列
              */
            loaded: function(status) {
                var array = [];
                var i = 0;
                 for (i = 0; i < this.aImg.length; i++) {
                    var hasClass = API.hasClass(this.aImg[i], 'loaded');
                     if (!status) {
                        if (!hasClass)
                            array.push(this.aImg[i])
                    }
                    if (status) {
                        if (hasClass)
                            array.push(this.aImg[i])
                    }
                }
                return array;       
            }
        };
        // 按需載入初始化
        API.on(window, 'load', function () {new lazyload()});
    </script>
</body>
</html>

執行上述的示例程式碼,並抓包會發現:一開始並沒有看到圖片的請求,但當拉動滾動條到頁面下面時,將會看到圖片傳送請求。目前很多框架都已經支援圖片的懶惰載入,平時在開發中,大家可以對圖片實現懶惰載入,這是有效提升效能的一個方法,特別是網頁圖片比較多時,更加應該使用該方法。
按需載入除了上述場景外,還有更多的場景。如下圖:
這裡寫圖片描述
頁面一開始,載入的是“全部”標籤裡面的內容,但在點選“指定商品折扣券”標籤時,才去載入對應的圖片。實現思路如下:
生成標籤時,用data-src來儲存圖片地址;
在點選標籤事件時,獲取所有圖片,圖片的src的值用data-src的來替換;
示例程式碼如下:

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>標籤按需載入</title>
</head>

<body>
    <style>
        ul li {width:200px;height:30px;float:left; list-style:none; text-align:center;}
        .on{border-top:1px solid #3FF;border-left:1px solid #3FF;border-right:1px solid #3FF; }
        .hide{display:none;}
    </style>
    <ul>
        <li>全部</li>
        <li id="viewTsb1" onclick="showTabContent()">指定商品折扣券</li>
    </ul>
    <div style="width:800px;height:500px;clear:both;">
        <div id="tab1" style="height:25px; line-height:25px; margin:50px 0 0 40px">全部標籤應該展示所有內容</div>
        <div id="tab2">
             <img width="158" height="158" data-src="http://img2.114msn.com/jindian/20081071153761308.jpg"  />
             <img width="158" height="158" data-src="http://www.mjjq.com/blog/photos/Image/mjjq-photos-900.jpg"  />
         </div>
    </div>
    <script>
        var isLoadedImg = false;
        function showTabContent(){ 
            if(isLoadedImg){
                return;
            }
            var elem = document.getElementById("tab2");
            document.getElementById("tab1").className="hide";
            elem.className="";
            var arrImage = elem.getElementsByTagName("img");
            var l = arrImage.length;
            for(var i=0;i<l;i++){
                arrImage[i].src = arrImage[i].getAttribute('data-src');
            }
            isLoadedImg = true;
            //todo 更改標籤狀態
        }
    </script>
</body>
</html>

執行上述程式碼並抓包並發現:一開始沒有看到有圖片的請求,但點選“指定商品折扣券”標籤時,看到有圖片的請求傳送。需要注意的是,為了確保體驗,首屏的圖片不建議懶惰載入,而應該直接展示出來;避免一開始使用者就無法看到圖片,在IE下看到一個虛線框,這樣體驗反而不好。
按需執行JS
按需執行JS和懶惰載入圖片比較類似。當開啟網頁時,如果等所有JS都載入並執行完畢,再把介面呈現給使用者,這樣整體上效能會比較慢,體驗也不友好。就是當某個動作觸發後,再執行相應的JS,以便來渲染介面。按需執行JS,可以應用在下列場景:執行一些耗時比較久的JS程式碼,或執行JS後,需要載入比較多圖片、載入iframe、載入廣告等。在一些webapp的應用中,或比較複雜的頁面時,更加應該使用這種方法。
實現思路和按需載入比較類似:
對滾動條進行事件繫結,假設繫結的函式為function lazyExecuteJS(){};
在函式lazyExecuteJS中,按照下面思路實現:選擇一個元素作為參照物,當滾動條即將靠近時該元素位置,開始執行對應的JS,從而實現對介面的渲染;
示例程式碼如下(以YUI3框架為例):
首先下載最近封裝的非同步滾動條載入元件:Y.asyncScrollLoader,然後執行下面的程式碼(需要把頁面和Y.asyncScrollLoader.js 放在同一個目錄):

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>按需執行JS</title>
</head>
<script src="http://yui.yahooapis.com/3.2.0/build/yui/yui-debug.js"></script>
<body>
    <div style="height:1500px"><span style="">向下拖動滾動條,拉到廣告條的位置,將會自動載入廣告的內容!</span></div>
    <div id="ADContent">這裡是廣告的內容,將會用JS進行填充</div>
    <script>
        YUI({modules:{'asyncScrollLoader': { 
            fullpath: 'Y.asyncScrollLoader.js', 
                type: 'js', 
                requires:['widget'] 
            } 
        }}).use('widget','asyncScrollLoader', "node", function(Y){
            var  loadAD = function(){
                //這裡可以用簡單的程式碼代替,實際專案中,可以執行任何JS程式碼,如請求CGI,或者廣告資料,然後再填充到頁面上
                var html = '<div><img src="http://t2.baidu.com/it/u=2027994158,3223530939&fm=23&gp=0.jpg" alt="" /><span>哈哈,我也是動態載入進來的,可以從html結構或抓包看出\效果哈!</span><\/div>'
                Y.one('#ADContent').set('innerHTML',html);
            }

            var cfg = { 
                'elementName' : 'div', 
                'className'     : 'lazy-load', 
                'contentAttribute' : '' ,
                'foldDistance': 10,
                'obCallback' : {
                    'funName' : loadAD,
                    'argument' : [],
                    'context' : this
                }
            };

           new Y.asyncScrollLoader(cfg).renderer(); 
        });      
    </script>
</body>
</html>

執行上述程式碼並抓包發現:開啟頁面時,是不沒有看到有對應的圖片請求,但當滾動條拉到一定位置時,loadAD的函式被執行。
按需載入JS
JavaScript無非就是script標籤引入頁面,但當專案越來越大的時候,單頁面引入N個js顯然不行,合併為單個檔案減少了請求數,但請求的檔案體積卻很大。這時候比較合理的做法就是按需載入。按需載入和按需執行JS比較類似,只不過要執行的JS變成了固定的“實現載入JS”的程式碼。按需載入實現的思路如下:
對滾動條進行事件繫結,假設繫結的函式為function lazyLoadJS(){};
在函式lazyLoadJS中,按照下面思路實現:選擇一個元素作為參照物,當滾動條即將靠近時該元素位置,開始執行載入對應JS;
在JS載入完畢後,開始執行相應的函式來渲染介面;
在實際專案中,可以根據需要設定一個目標距離,比如還有200畫素該元素即將進入可視區域;按需載入JS和按需執行JS比較類似,這裡就不再單獨提供示例程式碼了;大家可以在按需執行JS的中示例中,把loadAD函式更改為動態載入JS即可;
分屏展示
當一個網頁比較長,有好幾個螢幕,而且載入了大量的圖片、廣告等資原始檔時,分屏展示,可提升頁面效能和使用者體驗。其實分屏展示也可以從按需載入的的角度來看待,預設是載入第一螢幕的內容,當滾動條拉動即將到達下一個螢幕時,再開始渲染下個屏的內容。換言之,是把圖片、背景圖片、HTML一起按需載入,一開始不對HTML進行解析,那麼背景圖、img圖片也不會進行載入。
分屏展示的思路如下:
根據具體業務情況,收集主流最大的解析度的高度;假設這裡是用960px;
按照這個高度進行分屏,依次把下一個螢幕內的HTML用HTML

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>網頁分屏展示</title>
</head>
<style>
    .class2 {
        visibility:hidden;
        widh:100%;
        height:300px;
    }
</style>
<script src="http://yui.yahooapis.com/3.2.0/build/yui/yui-debug.js"></script>
<body>
<div style="height:1500px"><span style="">向下拖動滾動條,美圖在下面!</span></div>
<textarea>
    <div>
        <img src="http://travel.shangdu.com/liu/uploads/allimg/090407/1521221.jpg" alt="" /><span style="font-size:16px;color:red;">我是動態載入進來的,可以從html結構或抓包看出效果哈!</span>
    </div>
</textarea>
<div style="height:800px"><span style="">下面繼續上美圖!</span></div>
<textarea>
    <div>
        <img src="http://download.99sucai.com/upload/images/201006042009245.jpg"  alt="" /><span>哈哈,我也是動態載入進來的,可以從html結構或抓包看出效果哈!</span>
    </div>
</textarea>
<script>
 YUI({modules:{'asyncScrollLoader': { 
     fullpath: 'Y.asyncScrollLoader.js', 
     type: 'js', 
     requires:['widget'] 
     } 
 }}).use('widget','asyncScrollLoader', "node", function(Y){
     var cfg = { 
        'elementName' : 'textarea', 
        'className'     : 'class2', 
        'contentAttribute' : 'value' ,
        'foldDistance': 10           
    };        
    new Y.asyncScrollLoader(cfg).renderer(); 
 });      
</script>
</body>
</html>

執行上面程式碼並抓包發現:在預設首屏,並沒有去解析textarea裡面的程式碼,但當拉動滾動條到一定位置時,textarea裡面的HTML依次被解析,從而實現了網頁分屏展示。上述的示例程式碼,可以在這裡下載來檢視: 示例程式碼
使用“按需載入”進行效能優化時,需要合理選擇觸發的動作。“按需載入”的最大優勢在於減少了不必要的資源請求,節省流量,真正實現“按需所取”。但是“按需載入”本身如果使用不當也會影響使用者體驗,因為“按需載入”的時機在使用者觸發某動作之後,如果使用者的網速比較慢的話,載入指令碼或執行指令碼可能需要等候較長的時間,而使用者則不得不為此付出代價。因此,如果要使用“按需載入”則需要選擇正確的觸發動作,如果是根據滾動條來觸發,可考慮一個目標距離,假設目標距離還有200畫素即將進入可視區域,則就開始載入,而不是等到進入了可視區域才載入。以上所講的各種“按需載入”型別,都可以封裝成相應的元件,然後就可以在專案中進行應用。