疑點引入
不知道大家有沒有這樣去考慮一個問題:
在我們實際的項目中,我們一般都是使用一些打包工具(譬如grunt gulp webpack)將我們項目引入的第三方依賴與項目自己的邏輯js代碼分別打包成為一個js壓縮包。但是按道理來說,這兩個js文件在首屏渲染的時候應該都會加載下來,我們也可以通過控制台看到這樣。那這樣不還是相當於全部加載了嗎?哪里來的按需加載呢?另外,如果我的頁面一次性加載了全部文件,那之后是不是不需要再加載額外的文件了,豈不是也更快?那什么還需要按需加載呢?
什么是按需加載
按需加載是前端性能優化的一大措施。顧名思義,按需加載就是根據需要去加載資源。在js中,我們一般通過一些用戶行為或者定時任務去觸發一些加載動作。比如但不限於以下幾個情況:鼠標點擊、輸入文字、拉動滾動條,鼠標移動、窗口大小更改等。加載的文件,可以是JS、圖片、CSS、HTML等。這個就是按需加載。
為什么需要按需加載
我們都知道,瀏覽器在同一時間內的可以發出的請求數有限制,所以這也是我們采用第三方打包工具將多個文件打包為一個文件的意義。但是多個文件打包為一個文件時包又比較大,一次性下載下來的速度就比較慢,仍然會有剛進入單頁面系統產生首頁白屏時間較長的情況。這種用戶體驗也不好。按需加載可用較好的去解決這些問題。
按需加載html
其實在以前的多頁面系統內。本身就是一個按需加載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>
當頁面加載結束時,並沒有看到圖片的請求;當點“點我展示HTML”按鈕時,通過抓包發現有圖片請求。
曾經做個demo並經過測試發現,如果是直接解析HTML(不包含有請求CSS圖片和img標簽),耗費的時間要比用<script type=”text/x-template”>html</script>大約慢1-2倍,如果是還包括請求有CSS圖片、img標簽,請求連接數將會更多,可見按需解析HTML,對性能提升還是有一定效果。
二:圖片的按需加載
按需加載圖片,就是讓圖片默認開始不加載,而且在接近可視區域范圍時,再進行加載。也稱之為懶惰加載。大家都知道,圖片一下子全部都加載,請求的次數將會增加,勢必影響性能。
先來看下懶惰加載的實現原理。它的觸發動作是:當滾動條拉動到某個位置時,即將進入可視范圍的圖片需要加載。實現的過程分為下面幾個步驟:
- 生成<img data-src="http://tid.tenpay.com/”url”>標簽時,用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>
運行上述的示例代碼,並抓包會發現:一開始並沒有看到圖片的請求,但當拉動滾動條到頁面下面時,將會看到圖片發送請求。目前很多框架都已經支持圖片的懶惰加載,平時在開發中,大家可以對圖片實現懶惰加載,這是有效提升性能的一個方法,特別是網頁圖片比較多時,更加應該使用該方法。
按需加載除了上述場景外,還有更多的場景。如下圖:
頁面一開始,加載的是“全部”標簽里面的內容,但在點擊“指定商品折扣券”標簽時,才去加載對應的圖片。實現思路如下:
- 生成<img data-src="http://tid.tenpay.com/”url”>標簽時,用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="/imgr?src=http%3A%2F%2Ft2.baidu.com%2Fit%2Fu%3D2027994158%2C3223530939%26amp%3Bfm%3D23%26amp%3Bgp%3D0.jpg&r=http%3A%2F%2Ftid.tenpay.com%2F%3Fp%3D4085" 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用<textarea>HTML</’textarea> 來表示;
- 為了讓頁面的高度不變,需要讓textarea占據一定的頁面空間,也就是讓頁面出現對應的滾動條;因此需要指定樣式visility:hidden,並指定它的高度和寬度。
- 利用上述講的按需執行JS,把<textarea>HTML</’textarea>里面的HTML代碼提取出來,重新填充到textarea的父節點上,便可實現解析對應HTML,從而實現分屏展示。
示例代碼如下(需要把頁面和Y.asyncScrollLoader.js 放在同一個目錄):
<!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像素即將進入可視區域,則就開始加載,而不是等到進入了可視區域才加載。以上所講的各種“按需加載”類型,都可以封裝成相應的組件,然后就可以在項目中進行應用。