前端性能優化最佳實踐

現在瀏覽器可以實現的特性愈來愈多,而且網絡逐漸向移動設備轉移,使咱們的前端代碼更加緊湊,如何優化,就變得愈來愈重要了。php

開發人員廣泛會將他們的代碼習慣優先於用戶體驗。可是不少很小的改變可讓用戶體驗有個飛躍提高,因此任何一點兒小小的優化都會提高你網站的性能。css

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

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

DOM操做在瀏覽器上是要付稅的。儘管性能提高是在瀏覽器,DOM很慢,若是你沒有注意到,你可能會察覺瀏覽器運行很是的慢。這就是爲何減小建立集中的DOM節點以及快速注入是那麼的重要了。jquery

如今假設咱們頁面中有一個<ul>元素,調用AJAX獲取JSON列表,而後使用JavaScript更新元素內容。一般,程序員會這麼寫:程序員

Javascript代碼web

var list = document.querySelector('ul'); ajaxResult.items.forEach(function(item) { // 建立<li>元素  var li = document.createElement('li'); li.innerHTML = item.text; // <li>元素常規操做,例如添加class,更改屬性attribute,添加事件監聽等  // 迅速將<li>元素注入父級<ul>中  list.apppendChild(li); });

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

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

Javascript代碼數組

var frag = document.createDocumentFragment(); ajaxResult.items.forEach(function(item) { // 建立<li>元素  var li = document.createElement('li'); li.innerHTML = item.text; // <li>元素常規操做  // 例如添加class,更改屬性attribute,添加事件監聽,添加子節點等  // 將<li>元素添加到碎片中  frag.appendChild(li); }); // 最後將全部的列表對象經過DocumentFragment集中注入DOM  document.querySelector('ul').appendChild(frag);

爲DocumentFragment追加子元素,而後再將這個DocumentFragment加到父列表中,這一系列操做僅僅是一個DOM操做,所以它比起集中注入要快不少。

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

Javascript代碼

var htmlStr = ''; ajaxResult.items.forEach(function(item) { // 構建包含HTML頁面內容的字符串  htmlStr += '<li>' + item.text + '</li>'; }); // 經過innerHTML設定ul內容  document.querySelector('ul').innerHTML = htmlStr;

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

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

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

這就是爲何咱們要引入防抖。

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

Javascript代碼

// 取自 UnderscoreJS 實用框架  function debounce(func, wait, immediate) { var timeout; return function() { var context = this, args = arguments; var later = function() { timeout = null; if (!immediate) func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } // 添加resize的回調函數,可是隻容許它每300毫秒執行一次  window.addEventListener('resize', debounce(function(event) { // 這裏寫resize過程  }, 300));

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

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

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

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

Javascript代碼

define(function() { var cacheObj = window.sessionStorage || { getItem: function(key) { return this[key]; }, setItem: function(key, value) { this[key] = value; } }; return { get: function(key) { return this.isFresh(key); }, set: function(key, value, minutes) { var expDate = new Date(); expDate.setMinutes(expDate.getMinutes() + (minutes || 0)); try { cacheObj.setItem(key, JSON.stringify({ value: value, expires: expDate.getTime() })); } catch(e) { } }, isFresh: function(key) { // 返回值或者返回false  var item; try { item = JSON.parse(cacheObj.getItem(key)); } catch(e) {} if(!item) return false; // 日期算法  return new Date().getTime() > item.expires ? false : item.value; } } });

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

Javascript代碼

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

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

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

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

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

Javascript代碼

// 基本使用: 加載一部分AMD格式的模塊  curl(['social', 'dom'], function(social, dom) { dom.setElementContent('.social-container', social.loadWidgets()); }); // 定義一個使用Google Analytics的模塊,該模塊是非AMD格式的  define(["js!//google-analytics.com/ga.js"], function() { // Return a simple custom Google Analytics controller  return { trackPageView: function(href) { _gaq.push(["_trackPageview", url]); }, trackEvent: function(eventName, href) { _gaq.push(["_trackEvent", "Interactions", eventName, "", href || window.location, true]); } }; }); // 加載一個不帶回調方法的非AMD的js文件  curl(['js!//somesite.com/widgets.js']); // 將JavaScript和CSS文件做爲模塊加載  curl(['js!libs/prism/prism.js', 'css!libs/prism/prism.css'], function() { Prism.highlightAll(); }); // 加載一個AJAX請求的URL  curl(['text!sidebar.php', 'storage', 'dom'], function(content, storage, dom) { storage.set('sidebar', content, 60); dom.setElementContent('.sidebar', content); });

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

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

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

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

Javascript代碼

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

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

Javascript代碼 var items = []; ajaxResult.items.forEach(function(item) { // 構建字符串  items.push('<li>', item.text, '</li>'); }); // 經過innerHTML設置列表內容  document.querySelector('ul').innerHTML = items.join('');

也許你須要花上一點兒時間來看看這個數組是作什麼用的,可是全部的用戶都從這個優化中受益不淺。

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

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

Javascript代碼

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

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

Javascript代碼

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

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

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

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

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

Javascript代碼

// 獲取元素,添加事件監聽  document.querySelector('#parent-list').addEventListener('click', function(e) { // e.target 是一個被點擊的元素!  // 若是它是一個列表元素  if(e.target && e.target.tagName == 'LI') { // 咱們找到了這個元素,對他的操做能夠寫在這裏。  } });

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

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

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

Javascript代碼

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

固然頁面大小會增長(若是你的服務器使用適當的gzip內容,這個增長會很小),可是你減小了潛在的請求,同時也在過程當中減小了服務器請求的數量。如今大多數瀏覽器都支持Data URI,在CSS中的背景骨片也可使用Data URI,所以這個策略如今已經能夠在應用層級,普遍應用。

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

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

Javascript代碼

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

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

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

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

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

Javascript代碼

function getUser(desiredUsername) { var searchResult = ajaxResult.users.filter(function(user) { return user.username == desiredUsername; }); return searchResult.length ? searchResult[0] : false; } // 根據用戶名獲取用戶  var davidwalsh = getUser("davidwalsh"); // 根據用戶名獲取另外一個用戶.  var techpro = getuser("tech-pro");

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

Javascript代碼

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

如今當你想要找一個用戶對象時,咱們能夠直接經過索引找到這個對象:

Javascript代碼

var davidwalsh = userStore.davidwalsh; 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代碼

/* 使用Web Worker */ // 啓動worker  var worker = new Worker("/path/to/web/worker/resource.js"); worker.addEventListener("message", function(event) { // 咱們從web worker獲取信息!  }); // 指導Web Worker工做!  worker.postMessage({ cmd: "processImageData", data: convertImageToDataUri(myImage) }); /* resource.js就是一個Web workder */ self.addEventListener("message", function(event) { var data = event.data; switch (data.cmd) { case 'process': return processImageData(data.imageData); }); function processImageData(imageData) { // 對圖像進行操做  // 例如將它改爲灰度  return newImageData; }

以上這段代碼是一個教你如何使用Web Worker在其餘進程中作一些繁重工做的簡單示例。它要執行的是將一個圖片從普通顏色轉個灰度,由於這是一個比較繁重的過程,因此你能夠將這個進程提交給Web Worker,使你的瀏覽器負載不是很大。Data經過message事件傳回。
你能夠仔細閱讀如下MDN上關於Web Workder的使用,也許在你的網站上有一些功能能夠移到其餘的獨立進程中去執行。

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

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

Javascript代碼

// 主CSS文件(main.css)  @import "reset.css"; @import "structure.css"; @import "tutorials.css"; @import "contact.css"; // 而後在tutorials.css文件中,會繼續有@import  @import "document.css"; @import "syntax-highlighter.css";

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

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

在上面第13個最佳實踐中咱們說過,多個CSS文件能夠經過@import規則合併到一塊兒。可是不少程序員不知道的是,多種CSS介質類型也能夠合併到一個文件中。

Javascript代碼

/* 如下所有寫在一個CSS文件中 */ @media screen { /* 全部默認的結構設計和元素樣式寫在這裏 */ } @media print { /* 調整打印時的樣式 */ } @media only screen and (max-width : 1024px) { /* 使用ipad或者移動電話時的樣式設定 */ }

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

相關文章
相關標籤/搜索