现在浏览器可以实现的特性愈来愈多,而且网络逐渐向移动设备转移,使咱们的前端代码更加紧凑,如何优化,就变得愈来愈重要了。
开发人员广泛会将他们的代码习惯优先于用户体验。可是不少很小的改变可让用户体验有个飞跃提高,因此任何一点儿小小的优化都会提高你网站的性能。
前端给力的地方是能够有许多种简单的策略和代码习惯让咱们能够保证最理想的前端性能。咱们这个系列的主题就是要告诉你一些前端性能优化的最佳实践,只须要一分钟,就能够优化你现有的代码。
目 录 最佳实践1:使用DocumentFragments或innerHTML取代复杂的元素注入 最佳实践2:高频执行事件/方法的防抖 最佳实践3:网络存储的静态缓存和非必要内容优化 最佳实践4:使用异步加载,延迟加载依赖 最佳实践5:使用Array.prototype.join代替字符串链接 最佳实践6:尽量使用CSS动画 最佳实践7:使用事件委托 最佳实践8:使用Data URI代替图片SRC 最佳实践9:使用媒体查询加载指定大小的背景图片 最佳实践10:使用索引对象 最佳实践11:控制DOM大小 最佳实践12:在繁重的执行上使用Web Workers 最佳实践13:连接CSS,避免使用@import 最佳实践14:在CSS文件中包含多种介质类型
最佳实践1:使用DocumentFragments或innerHTML取代复杂的元素注入 DOM操做在浏览器上是要付税的。尽管性能提高是在浏览器,DOM很慢,若是你没有注意到,你可能会察觉浏览器运行很是的慢。这就是为何减小建立集中的DOM节点以及快速注入是那么的重要了。
如今假设咱们页面中有一个<ul>元素,调用AJAX获取JSON列表,而后使用JavaScript更新元素内容。一般,程序员会这么写: php
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。
DocumentFragement 是一组子节点的“虚拟存储”,而且它没有父标签。在咱们的例子中,将DocumentFragement想象成看不见的<ul>元素,在 DOM外,一直保管着你的子节点,直到他们被注入DOM中。那么,原来的代码就能够用DocumentFragment优化一下: css
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内容:html
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事件 - 他们触发时,执行的很是迅速,而且触发不少次。若是你的回调太重,你可能使浏览器死掉。
这就是为何咱们要引入防抖。
防抖能够限制一个方法在必定时间内执行的次数。如下代码是个防抖示例:前端
// 取自 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使这些内容加载更加简单:jquery
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方法同样,保证了存储的数据不会过时。调用方法也很是简单: 程序员
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工具来提高你网站的性能。web
最佳实践4:使用异步加载,延迟加载依赖
RequireJS已经迎来了异步加载和AMD格式的巨大浪潮。XMLHttpRequest(该对象能够调用AJAX)使得资源的异步加载变得流行起来,它容许无阻塞资源加载,而且使 onload 启动更快,容许页面内容加载,而不须要刷新页面。
我所用的异步加载器是John Hann的curl。curl加载器是基本的异步加载器,能够被配置,拥有很好的插件。如下是一小段curl的代码:ajax
// 基本使用: 加载一部分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”中,我在代码中使用了基本字符链接: 算法
htmlStr += '<li>' + item.text + '</li>';
可是下面这段代码中,我用了优化:数组
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强制使你的硬件加速:
.myAnimation { animation: someAnimation 1s; transform: translate3d(0, 0, 0); /* 强制硬件加速 */ }
tansform:transform(0,0,0)在不会影响其余动画的同时将通话送入硬件加速。在不支持CSS动画的状况下(IE8及如下版本的浏览器),你能够引入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会评估相应的措施是否须要被执行。下面咱们给出了一个简单的例子:
// 获取元素,添加事件监听 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属性:
<!-- 之前的写法 --> <img src="http://images.cnblogs.com/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属性),例如根据不一样的屏幕宽度来设置不一样的元素宽度或者是悬浮位置。那么咱们为何不用这种方式来改变背景图片呢?
/* 默认是为桌面应用加载图片 */ .someElement { background-image: url(sunset.jpg); } @media only screen and (max-width : 1024px) { .someElement { background-image: url(sunset-small.jpg); } }
上面的代码片断是为手机设备或是相似的移动设备加载一个较小尺寸的图片,特别是须要一个特别小的图片时(例如图片的大小几乎不可视)。
最佳实践10:使用索引对象 这一篇,咱们将讲讲使用索引对象检索代替遍历数组,提升遍历速度。 AJAX和JSON一个最多见的使用案例是接收包含一组对象的数组,而后从这组数组中根据给定的值搜索对象。让咱们看一个简单的例子,下面这个例子中,你从用户接收一个数组,而后你能够根据username的值来搜索用户对象:
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做为索引,这个数组对象能够写成:
var userStore = {}; ajaxResult.users.forEach(function(user) { userStore[user.username] = user; });
如今当你想要找一个用户对象时,咱们能够直接经过索引找到这个对象:
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大小的技巧包括:
简单一句话:尽可能使你的DOM越小越好。
最佳实践12:在繁重的执行上使用Web Workers 这一篇咱们将介绍Web Workder,一种能够将繁重操做移到独立进程的方法。 Web Workders在前段时间被引入流行的浏览器中,可是好像并无被普遍应用。Web Workers的主要功能是在通常浏览器执行范围外执行繁重的方法。它不会访问DOM,因此你必须传入方法涉及的节点。 如下是一段Web Workder的示例代码:
/* 使用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规则每每会形成事件嵌套:
// 主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介质类型也能够合并到一个文件中。
/* 如下所有写在一个CSS文件中 */ @media screen { /* 全部默认的结构设计和元素样式写在这里 */ } @media print { /* 调整打印时的样式 */ } @media only screen and (max-width : 1024px) { /* 使用ipad或者移动电话时的样式设定 */ }
对于文件的大小,何时必须合并介质,或是何时必须分开设定,CSS并无硬性规定,可是我会比较建议将全部的介质合并,
除非其中一个介质所占的比例比起其余大了许多。少一个请求对于客户端和服务器都将轻松很多,并且在大多数状况下,附赠的介质类型相比主屏介质类型要相对小不少。