前端性能優化

如今瀏覽器能夠實現的特性越來越多,並且網絡逐漸向移動設備轉移,使我們的前端代碼更加緊湊,如何優化,就變得越來越重要了。 


開發人員普遍會將他們的代碼習慣優先於用戶體驗。但是很多很小的改變可以讓用戶體驗有個飛躍提升,所以任何一點兒小小的優化都會提升你網站的性能。 


前端給力的地方是可以有許多種簡單的策略和代碼習慣讓我們可以保證最理想的前端性能。我們這個系列的主題就是要告訴你一些前端性能優化的最佳實踐,只需要一分鐘,就可以優化你現有的代碼。

最佳實踐1:使用DocumentFragments或innerHTML取代複雜的元素注入


DOM操作在瀏覽器上是要付稅的。儘管性能提升是在瀏覽器,DOM很慢,如果你沒有注意到,你可能會察覺瀏覽器運行非常的慢。這就是爲什麼減少創建集中的DOM節點以及快速注入是那麼的重要了。 


現在假設我們頁面中有一個<ul>元素,調用AJAX獲取JSON列表,然後使用JavaScript更新元素內容。通常,程序員會這麼寫: 


Javascript代碼 

  1. var list = document.querySelector('ul');  
  2. ajaxResult.items.forEach(function(item) {  
  3.     // 創建<li>元素  
  4.     var li = document.createElement('li');  
  5.     li.innerHTML = item.text;  
  6.   
  7.     // <li>元素常規操作,例如添加class,更改屬性attribute,添加事件監聽等  
  8.   
  9.     // 迅速將<li>元素注入父級<ul>中  
  10.     list.apppendChild(li);  
  11. });  



上面的代碼其實是一個錯誤的寫法,將<ul>元素帶着對每一個列表的DOM操作一起移植是非常慢的。如果你真的想要 使用document.createElement,並且將對象當做節點來處理,那麼考慮到性能問題,你應該使用DocumentFragement。 


DocumentFragement 是一組子節點的“虛擬存儲”,並且它沒有父標籤。在我們的例子中,將DocumentFragement想象成看不見的<ul>元素,在 DOM外,一直保管着你的子節點,直到他們被注入DOM中。那麼,原來的代碼就可以用DocumentFragment優化一下: 


Javascript代碼 

  1. var frag = document.createDocumentFragment();  
  2.   
  3. ajaxResult.items.forEach(function(item) {  
  4.     // 創建<li>元素  
  5.     var li = document.createElement('li');  
  6.     li.innerHTML = item.text;  
  7.   
  8.     // <li>元素常規操作  
  9.     // 例如添加class,更改屬性attribute,添加事件監聽,添加子節點等  
  10.   
  11.     // 將<li>元素添加到碎片中  
  12.     frag.appendChild(li);  
  13. });  
  14.   
  15. // 最後將所有的列表對象通過DocumentFragment集中注入DOM  
  16. document.querySelector('ul').appendChild(frag);  



爲DocumentFragment追加子元素,然後再將這個DocumentFragment加到父列表中,這一系列操作僅僅是一個DOM操作,因此它比起集中注入要快很多。 


如果你不需要將列表對象當做節點來操作,更好的方法是用字符串構建HTML內容: 


Javascript代碼 

  1. var htmlStr = '';  
  2.   
  3. ajaxResult.items.forEach(function(item) {  
  4.     // 構建包含HTML頁面內容的字符串  
  5.     htmlStr += '<li>' + item.text + '</li>';  
  6. });  
  7.   
  8. // 通過innerHTML設定ul內容  
  9. document.querySelector('ul').innerHTML = htmlStr;  



這當中也只有一個DOM操作,並且比起DocumentFragment代碼量更少。在任何情況下,這兩種方法都比在每一次迭代中將元素注入DOM更高效。

最佳實踐2:高頻執行事件/方法的防抖


通常,開發人員會在有用戶交互參與的地方添加事件,而往往這種事件會被頻繁觸發。想象一下窗口的resize事件或者是一個元素的onmouseover事件 - 他們觸發時,執行的非常迅速,並且觸發很多次。如果你的回調過重,你可能使瀏覽器死掉。 


這就是爲什麼我們要引入防抖。 


防抖可以限制一個方法在一定時間內執行的次數。以下代碼是個防抖示例: 


Javascript代碼 

  1. // 取自 UnderscoreJS 實用框架  
  2. function debounce(func, wait, immediate) {  
  3.     var timeout;  
  4.     return function() {  
  5.         var context = this, args = arguments;  
  6.         var later = function() {  
  7.             timeout = null;  
  8.             if (!immediate) func.apply(context, args);  
  9.         };  
  10.         var callNow = immediate && !timeout;  
  11.         clearTimeout(timeout);  
  12.         timeout = setTimeout(later, wait);  
  13.         if (callNow) func.apply(context, args);  
  14.     };  
  15.   }  
  16.   
  17. // 添加resize的回調函數,但是隻允許它每300毫秒執行一次  
  18. window.addEventListener('resize', debounce(function(event) {  
  19.   
  20.     // 這裏寫resize過程  
  21.   
  22. }, 300));  



debounce方法返回一個方法,用來包住你的回調函數,限制他的執行頻率。使用這個防抖方法,就可以讓你寫的頻繁回調的方法不會妨礙用戶的瀏覽器!

最佳實踐3:網絡存儲的靜態緩存和非必要內容優化


Web Storage的API曾經是Cookie API一個顯著的進步,並且爲開發者使用了很多年了。這個API是合理的,更大存儲量的,而且是更爲健全理智的。一種策略是去使用Session存儲來存 儲非必要的,更爲靜態的內容,例如側邊欄的HTML內容,從Ajax加載進來的文章內容,或者一些其他的各種各樣的片斷,是我們只想請求一次的。 


我們可以使用JavaScript編寫一段代碼,利用Web Storage使這些內容加載更加簡單: 


Javascript代碼 

  1. define(function() {  
  2.   
  3.     var cacheObj = window.sessionStorage || {  
  4.         getItem: function(key) {  
  5.             return this[key];  
  6.         },  
  7.         setItem: function(key, value) {  
  8.             this[key] = value;  
  9.         }  
  10.     };  
  11.   
  12.     return {  
  13.         get: function(key) {  
  14.             return this.isFresh(key);  
  15.         },  
  16.         set: function(key, value, minutes) {  
  17.             var expDate = new Date();  
  18.             expDate.setMinutes(expDate.getMinutes() + (minutes || 0));  
  19.   
  20.             try {  
  21.                 cacheObj.setItem(key, JSON.stringify({  
  22.                     value: value,  
  23.                     expires: expDate.getTime()  
  24.                 }));  
  25.             }  
  26.             catch(e) { }  
  27.         },  
  28.         isFresh: function(key) {  
  29.             // 返回值或者返回false  
  30.             var item;  
  31.             try {  
  32.                 item = JSON.parse(cacheObj.getItem(key));  
  33.             }  
  34.             catch(e) {}  
  35.             if(!item) return false;  
  36.   
  37.             // 日期算法  
  38.             return new Date().getTime() > item.expires ? false : item.value;  
  39.         }  
  40.      }  
  41. });  



這個工具提供了一個基礎的get和set方法,同isFresh方法一樣,保證了存儲的數據不會過期。調用方法也非常簡單: 


Javascript代碼 

  1. require(['storage'], function(storage) {  
  2.     var content = storage.get('sidebarContent');  
  3.     if(!content) {  
  4.         // Do an AJAX request to get the sidebar content  
  5.   
  6.         // ... and then store returned content for an hour  
  7.         storage.set('sidebarContent', content, 60);   
  8.     }  
  9. });  



現在同樣的內容不會被重複請求,你的應用運行的更加有效。花一點兒時間,看看你的網站設計,將那些不會變化,但是會被不斷請求的內容挑出來,你可以使用Web Storage工具來提升你網站的性能。

最佳實踐4:使用異步加載,延遲加載依賴


RequireJS已經迎來了異步加載和AMD格式的巨大浪潮。XMLHttpRequest(該對象可以調用AJAX)使得資源的異步加載變得流行起來,它允許無阻塞資源加載,並且使 onload 啓動更快,允許頁面內容加載,而不需要刷新頁面。 


我所用的異步加載器是John Hann的curl。curl加載器是基本的異步加載器,可以被配置,擁有很好的插件。以下是一小段curl的代碼: 


Javascript代碼 

  1. // 基本使用:  加載一部分AMD格式的模塊  
  2. curl(['social''dom'], function(social, dom) {  
  3.     dom.setElementContent('.social-container', social.loadWidgets());  
  4. });  
  5.   
  6. // 定義一個使用Google Analytics的模塊,該模塊是非AMD格式的  
  7. define(["js!//google-analytics.com/ga.js"], function() {  
  8.     // Return a simple custom Google Analytics controller  
  9.     return {  
  10.         trackPageView: function(href) {  
  11.             _gaq.push(["_trackPageview", url]);  
  12.         },  
  13.         trackEvent: function(eventName, href) {  
  14.             _gaq.push(["_trackEvent""Interactions", eventName, "", href || window.location, true]);  
  15.         }  
  16.     };  
  17. });  
  18.   
  19. // 加載一個不帶回調方法的非AMD的js文件  
  20. curl(['js!//somesite.com/widgets.js']);  
  21.   
  22. // 將JavaScript和CSS文件作爲模塊加載  
  23. curl(['js!libs/prism/prism.js''css!libs/prism/prism.css'], function() {  
  24.     Prism.highlightAll();  
  25. });  
  26.   
  27. // 加載一個AJAX請求的URL  
  28. curl(['text!sidebar.php''storage''dom'], function(content, storage, dom) {  
  29.     storage.set('sidebar', content, 60);  
  30.     dom.setElementContent('.sidebar', content);  
  31. });  



你可能早就瞭解,異步加載可以大大提高萬展速度,但是我想在此說明的是,你要使用異步加載!使用了之後你可以看到區別,更重要的是,你的用戶可以看到區別。 


當你可以根據頁面內容延遲加載依賴的時候,你就可以體會到異步加載的好處了。例如,你可以只加載Twitter,Facebook和Google Plus到應用了名爲social的CSS樣式的div元素中。“在加載前檢查是否需要”策略可以爲我的用戶節省好幾KB的莫須有的加載。

最佳實踐5:使用Array.prototype.join代替字符串連接


有一種非常簡單的客戶端優化方式,就是用Array.prototype.join代替原有的基本的字符連接的寫法。在上面的“最佳實踐1”中,我在代碼中使用了基本字符連接: 


Javascript代碼 

  1. htmlStr += '<li>' + item.text + '</li>';  



但是下面這段代碼中,我用了優化: 


Javascript代碼 

  1. var items = [];  
  2.   
  3. ajaxResult.items.forEach(function(item) {  
  4.     // 構建字符串  
  5.     items.push('<li>', item.text, '</li>');  
  6. });  
  7.   
  8. // 通過innerHTML設置列表內容  
  9. document.querySelector('ul').innerHTML = items.join('');   



也許你需要花上一點兒時間來看看這個數組是做什麼用的,但是所有的用戶都從這個優化中受益匪淺。

最佳實踐6:儘可能使用CSS動畫


網站設計對美觀特性和可配置元素動畫的大量需求,使得一些JavaScript類庫,如jQuery,MooTools大量的被使用。儘管現在瀏覽器支持CSS的transformation和keyframe所做的動畫,現在仍有很多人使用JavaScript製作動畫效果,但是實際上使用CSS動畫比起JavaScript驅動的動畫效率更高。CSS動畫同時需要更少的代碼。很多的CSS動畫是用GPU處理的,因此動畫本身很流暢,當然你可以使用下面這個簡單的CSS強制使你的硬件加速: 


Javascript代碼 

  1. .myAnimation {  
  2.     animation: someAnimation 1s;  
  3.     transform: translate3d(0, 0, 0); /* 強制硬件加速 */  
  4. }  



tansform:transform(0,0,0)在不會影響其他動畫的同時將通話送入硬件加速。在不支持CSS動畫的情況下(IE8及以下版本的瀏覽器),你可以引入JavaScript動畫邏輯: 


Javascript代碼 

  1. <!--[if 低於IE8版本]>  
  2. <script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>  
  3. <script src="/js/ie-animations.js"></script>  
  4. <![endif]-->  



在上例中,ie-animations.js文件必須包含你自定義的jQuery代碼,用於當CSS動畫在早期IE中不被支持的情況下,來替代CSS動畫完成動畫效果。完美的通過CSS動畫來優化動畫,通過JavaScript來支持全局動畫效果。

最佳實踐7:使用事件委託


想象一下,如果你有一個無序列表,裏面有一堆<li>元素,每一個<li>元素都會在點擊的時候觸發一個行爲。這個時候,你通常會在每一個元素上添加一個事件監聽,但是如果當這個元素或者你添加了監聽的這個對象會被頻繁的移除添加呢?這個時候,你在移除添加元素的同時需要處理事件監聽的移除和添加。這個時候,我們就需要引入事件委託了。 


事件委託是在父級元素上添加一個事件監聽,來替代在每一個子元素上添加事件監聽。當事件被觸發時,event.target會評估相應的措施是否需要被執行。下面我們給出了一個簡單的例子: 


Javascript代碼 

  1. // 獲取元素,添加事件監聽  
  2. document.querySelector('#parent-list').addEventListener('click'function(e) {  
  3.     // e.target 是一個被點擊的元素!  
  4.     // 如果它是一個列表元素  
  5.     if(e.target && e.target.tagName == 'LI') {  
  6.         // 我們找到了這個元素,對他的操作可以寫在這裏。  
  7.     }  
  8. });  



上面的例子是不可思議的簡單,當事件發生的時候,它沒有輪詢父節點去尋找匹配的元素或選擇器,且它不支持基於選擇器的查詢(例如用class name,或者id來查詢)。所有的JavaScript框架提供了委託選擇器匹配。重點是,你避免了爲每一個元素加載事件監聽,而是在父元素上加一個事件監聽。這樣大大的增加了效率,並且減少了很多維護!

最佳實踐8:使用Data URI代替圖片SRC


提升頁面大小的效率,不僅僅是取決於使用精靈或是壓縮代碼,給定頁面的請求數量在前端性能中也佔有了很不小的重量。減少請求可以讓你的網站加載更快,而其中一種減少頁面請求的方法就是用Data URI代替圖片的src屬性: 


Javascript代碼 

  1. <!-- 以前的寫法 -->  
  2. <img src="/images/logo.png" />  
  3.    
  4. <!-- 使用data URI的寫法 -->  
  5. <img src="data: image/jpeg;base64,/9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAAPAAA/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoKDBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8fHx8fHx8fHx8fHx8fH ...." />  
  6.    
  7. <-- 範例:  http://davidwalsh.name/demo/data-uri-php.php -->  



當然頁面大小會增加(如果你的服務器使用適當的gzip內容,這個增加會很小),但是你減少了潛在的請求,同時也在過程中減少了服務器請求的數量。現在大多數瀏覽器都支持Data URI,在CSS中的背景骨片也可以使用Data URI,因此這個策略現在已經可以在應用層級,廣泛應用。

最佳實踐9:使用媒體查詢加載指定大小的背景圖片


直到CSS @supports被廣泛支持,CSS媒體查詢的使用接近於CSS中寫邏輯控制。我們經常用CSS媒體查詢來根據設備調整CSS屬性(通常根據屏幕寬度調整CSS屬性),例如根據不同的屏幕寬度來設置不同的元素寬度或者是懸浮位置。那麼我們爲什麼不用這種方式來改變背景圖片呢? 


Javascript代碼 

  1. /* 默認是爲桌面應用加載圖片 */  
  2. .someElement { background-image: url(sunset.jpg); }  
  3.    
  4. @media only screen and (max-width : 1024px) {  
  5.     .someElement { background-image: url(sunset-small.jpg); }  
  6. }  



上面的代碼片段是爲手機設備或是類似的移動設備加載一個較小尺寸的圖片,特別是需要一個特別小的圖片時(例如圖片的大小幾乎不可視)。

最佳實踐10:使用索引對象


這一篇,我們將講講使用索引對象檢索代替遍歷數組,提高遍歷速度。 


AJAX和JSON一個最常見的使用案例是接收包含一組對象的數組,然後從這組數組中根據給定的值搜索對象。讓我們看一個簡單的例子,下面這個例子中,你從用戶接收一個數組,然後你可以根據username的值來搜索用戶對象: 


Javascript代碼 

  1. function getUser(desiredUsername) {  
  2.     var searchResult = ajaxResult.users.filter(function(user) {  
  3.         return user.username == desiredUsername;  
  4.     });  
  5.    
  6.     return searchResult.length ? searchResult[0] : false;  
  7. }  
  8.    
  9. // 根據用戶名獲取用戶  
  10. var davidwalsh = getUser("davidwalsh");  
  11.    
  12. // 根據用戶名獲取另一個用戶.  
  13. var techpro = getuser("tech-pro");  



上面這段代碼可以運行,但是並不是很有效,當我們想要獲取一個用戶時,我們就要遍歷一次數組。那麼更好的方法是創建一個新的對象,對每一個唯一的值建立一個索引,在上面這個例子中,用username作爲索引,這個數組對象可以寫成: 


Javascript代碼 

  1. var userStore = {};  
  2. ajaxResult.users.forEach(function(user) {  
  3.     userStore[user.username] = user;  
  4. });  



現在當你想要找一個用戶對象時,我們可以直接通過索引找到這個對象: 


Javascript代碼 

  1. var davidwalsh = userStore.davidwalsh;  
  2. var techpro = userStore["tech-pro"];  



這樣的代碼寫起來更好一些,也很簡便,通過索引搜索比起遍歷整個數組更加快捷。

最佳實踐11:控制DOM大小


這一篇中,我們要說如何控制DOM的大小,來優化前端性能。 


DOM很慢是衆所周知的,使得網站變慢的罪魁禍首是大量的DOM。想象一下,假如你有一個有着上千節點的DOM,在想象一下,使用querySelectorAll或者getElementByTagName,或者是其他以DOM爲中心的搜索方式來搜索一個節點,即使是使用內置方法,這也將是一個非常費力的過程。你要知道,多餘的DOM節點會使其他的實用程序也變慢的。 


我見過的一種情況,DOM的大小悄然增加,是在一個AJAX網站,它將所有的頁面都存在了DOM中,當一個新的頁面通過AJAX被加載時,舊的頁面就會被存入隱藏的DOM節點。對於DOM的速度,將有災難性的降低,特別是當一個頁面是動態加載的。所以你需要一種更好的方法。 


在這種情況下,當頁面是通過AJAX加載的,並且以前的頁面是存儲在客戶端的,最好的方法就是將內容通過String HTML存儲(將內容從DOM中移除),然後使用事件委託來避免特定元素事件。這麼做的同時,當在客戶端緩存內容的時候,可以避免大量的DOM生成。 

通常控制DOM大小的技巧包括: 


  • 使用:before和:after僞元素
  • 延遲加載和呈現內容
  • 使用事件委託,更簡便的將節點轉換成字符串存儲

簡單一句話:儘量使你的DOM越小越好。

最佳實踐12:在繁重的執行上使用Web Workers


這一篇我們將介紹Web Workder,一種可以將繁重操作移到獨立進程的方法。 


Web Workders在前段時間被引入流行的瀏覽器中,但是好像並沒有被廣泛應用。Web Workers的主要功能是在一般瀏覽器執行範圍外執行繁重的方法。它不會訪問DOM,所以你必須傳入方法涉及的節點。 


以下是一段Web Workder的示例代碼: 


Javascript代碼 

  1. /* 使用Web Worker */  
  2. //  啓動worker  
  3. var worker = new Worker("/path/to/web/worker/resource.js");  
  4. worker.addEventListener("message"function(event) {  
  5.     // 我們從web worker獲取信息!  
  6. });  
  7.    
  8. // 指導Web Worker工作!  
  9. worker.postMessage({ cmd: "processImageData", data: convertImageToDataUri(myImage) });  
  10.    
  11. /*  resource.js就是一個Web workder */  
  12. self.addEventListener("message"function(event) {  
  13.     var data = event.data;  
  14.    
  15.     switch (data.cmd) {  
  16.         case 'process':  
  17.             return processImageData(data.imageData);  
  18. });  
  19.    
  20. function processImageData(imageData) {  
  21.     // 對圖像進行操作  
  22.     // 例如將它改成灰度  
  23.    
  24.     return newImageData;  
  25. }  



以上這段代碼是一個教你如何使用Web Worker在其他進程中做一些繁重工作的簡單示例。它要執行的是將一個圖片從普通顏色轉個灰度,因爲這是一個比較繁重的過程,所以你可以將這個進程提交給Web Worker,使你的瀏覽器負載不是很大。Data通過message事件傳回。 

你可以仔細閱讀以下MDN上關於Web Workder的使用,也許在你的網站上有一些功能可以移到其他的獨立進程中去執行。

最佳實踐13:鏈接CSS,避免使用@import


有時候,@import太好用以至於很難抗拒它的誘惑,但是爲了減少令人抓狂的請求,你必須要拒絕它!最常見的用法是在一個"main"CSS文件中,沒有任何的內容,只有@import規則。有時,多個@import規則往往會造成事件嵌套: 


Javascript代碼 

  1. // 主CSS文件(main.css)  
  2. @import "reset.css";  
  3. @import "structure.css";  
  4. @import "tutorials.css";  
  5. @import "contact.css";  
  6.    
  7. // 然後在tutorials.css文件中,會繼續有@import  
  8. @import "document.css";  
  9. @import "syntax-highlighter.css";  



我們這樣寫CSS文件,在文件中多了兩個多餘鏈接,因此會使頁面加載變慢。SASS可以讀取@import語句,鏈接CSS內容到一個文件中,減少了多餘的請求,控制了CSS文件的大小。

最佳實踐14:在CSS文件中包含多種介質類型


在上面第13個最佳實踐中我們說過,多個CSS文件可以通過@import規則合併到一起。但是很多程序員不知道的是,多種CSS介質類型也可以合併到一個文件中。 


Javascript代碼 

  1. /* 以下全部寫在一個CSS文件中 */  
  2.    
  3. @media screen {  
  4.     /* 所有默認的結構設計和元素樣式寫在這裏 */  
  5. }  
  6.    
  7. @media print {  
  8.     /* 調整打印時的樣式 */  
  9. }  
  10.    
  11. @media only screen and (max-width : 1024px) {  
  12.     /* 使用ipad或者移動電話時的樣式設定 */  
  13. }  



對於文件的大小,什麼時候必須合併介質,或是什麼時候必須分開設定,CSS並沒有硬性規定,但是我會比較建議將所有的介質合併,除非其中一個介質所佔的比例比起其他大了許多。少一個請求對於客戶端和服務器都將輕鬆不少,而且在大多數情況下,附贈的介質類型相比主屏介質類型要相對小很多。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章