前端网页性能最佳实践
你愿意为打开一个网页等待多长时间?我一秒也不愿意等。但是事实上大多数网站在响应速度方面都让人失望。现在越来越多的人开始建立自己的网站,博客,你的网页响应速度如何呢?在这篇文章中我们来介绍一下提高网页性能的最佳实践,以及相应的问题解决方案,让站长或者即将要成为站长的朋友了解如何去测试和提高网站响应速度,对自己的网站更有信心。
最佳实践
最佳实践我们引用的来自yahoo前端性能团队总结的35条黄金定律。原文猛击这里。下面我们分门别类将每条的关键点总结一下。
网页内容 服务器 Cookie CSS Javascript 图片 移动客户端网页内容
减少http请求次数
80%的响应时间花在下载网页内容(images, stylesheets, javascripts, scripts, flash等)。减少请求次数是缩短响应时间的关键!可以通过简化页面设计来减少请求次数,但页面内容较多可以采用以下技巧。
1. 捆绑文件: 现在有很多现成的库可以帮你将多个脚本文件捆绑成一个文件,将多个样式表文件捆绑成一个文件,以此来减少文件的下载次数。例如在asp.net中可以使用ScriptManager,asp.net MVC中的Bundling。
2. CSS Sprites: 就是把多个图片拼成一副图片,然后通过CSS来控制在什么地方具体显示这整张图片的什么位置。给大家看个熟悉的Sprites实例。
豆瓣把他的图标集中在一起,然后我们看他如何控制只显示第一个图标的
.app-icon-read {: 0 0;
background-position
}: url("/pics/app/app_icons_50_5.jpg") no-repeat scroll 0 0 transparent;
.app-icon {
background
: 50px;
border-radius: 10px 10px 10px 10px;
box-shadow: 1px 1px 2px #999999;
display: inline-block;
height
width
: 50px;}
3. Image Maps: 也是将多幅图拼在一起,然后通过坐标来控制显示导航。这里有个经典的例子,选中图片中的某个人就会将你带到不同的链接。
4. Inline images: 通过编码的字符串将图片内嵌到网页文本中。例如下面的inline image的显示效果为一个勾选的checkbox。
.sample-inline-png {
padding-left: 20px;
background: white url('data:image/png;base64,iVBORw0KGgoAA
AANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0l
EQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6
P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC') no-repeat scroll left top;
}
图片显示效果如左图
减少DNS查询次数
DNS查询也消耗响应时间,如果我们的网页内容来自各个不同的domain (比如嵌入了开放广告,引用了外部图片或脚本),那么客户端首次解析这些domain也需要消耗一定的时间。DNS查询结果缓存在本地系统和浏览器中一段时间,所以DNS查询一般是对首次访问响应速度有所影响。下面是我清空本地dns后访问博客园主页dns的查询请求。看少去还不少哦。
避免页面跳转
当客户端收到服务器的跳转回复时,客户端再次根据服务器回复中的location指定的地址再次发送请求,例如以下跳转回复。
HTTP/1.1 301 Moved Permanently
Location: http://example.com/newuri
Content-Type: text/html
当客户端遇到这种回复的时候,用户只能等待客户端再次发送请求,有的网站甚至会一直跳n次,跳到他想带你去的地方…当然在这个时候用户看不到任何页面内容,只有浏览器的进度条一直在刷新。
缓存Ajax
Ajax可以帮助我们异步的下载网页内容,但是有些网页内容即使是异步的,用户还是在等待它的返回结果,例如ajax的返回是用户联系人的下拉列表。所以我们还是要注意尽量应用以下规则提高ajax的响应速度。
- 添加Expires 或 Cache-Control报文头使回复可以被客户端缓存
- 压缩回复内容
- 减少dns查询
- 精简javascript
- 避免跳转
- 配置Etags
延迟加载
这里讨论延迟加载需要我们知道我们的网页最初加载需要的最小内容集是什么。剩下的内容就可以推到延迟加载的集合中。
Javascript是典型的可以延迟加载内容。一个比较激进的做法是开发网页时先确保网页在没有Javascript的时候也可以基本工作,然后通过延迟加载脚本来完成一些高级的功能。
提前加载
与延迟加载目的相反,提前加载的是为了提前加载接下来网页中访问的资源,下面是提前加载的类型
无条件提前加载:当前网页加载完成后,马上去下载一些其他的内容。例如google会在页面加载成功之后马上去下载一个所有结果中会用到的image sprite。
有条件加载:根据用户的输入推断需要加载的内容,雅虎的示例是search.yahoo.com,
有预期的的加载:这种情况一般发生在网页重新设计时,由于用户经常访问旧网页,本地对旧的网页内容缓存充分从而显得旧网页速度很快,而新的网页内容却没有缓存,设计者可以在旧网页的内容中预先加载一些新网页中可能用到的内容,这样新的网页就会生下来一些需要下载的资源。
减少DOM元素数量
网页中元素过多对网页的加载和脚本的执行都是沉重的负担,500个元素和5000个元素在加载速度上会有很大差别。
想知道你的网页中有多少元素,通过在浏览器中的一条简单命令就可以算出,
document.getElementsByTagName('*').length
多少算是多了呢?雅虎在写这篇文章的时候号称主页只有700多元素,但现在接近多了一倍。我们的网页至少别比雅虎还多吧。。。
根据域名划分内容
浏览器一般对同一个域的下载连接数有所限制,按照域名划分下载内容可以浏览器增大并行下载连接,但是注意控制域名使用在2-4个之间,不然dns查询也是个问题。
一般网站规划会将静态资源放在类似于static.example.com,动态内容放在www.example.com上。这样做还有一个好处是可以在静态的域名上避免使用cookie。后面我们会在cookie的规则中提到。
减少iframe数量
使用iframe要注意理解iframe的优缺点
优点
- 可以用来加载速度较慢的内容,例如广告。
- 安全沙箱保护。浏览器会对iframe中的内容进行安全控制。
- 脚本可以并行下载
缺点
- 即使iframe内容为空也消耗加载时间
- 会阻止页面加载
- 没有语义
避免404
404我们都不陌生,代表服务器没有找到资源,我们要特别要注意404的情况不要在我们提供的网页资源上,客户端发送一个请求但是服务器却返回一个无用的结果,时间浪费掉了。
更糟糕的是我们网页中需要加载一个外部脚本,结果返回一个404,不仅阻塞了其他脚本下载,下载回来的内容(404)客户端还会将其当成Javascript去解析。
服务器
使用CDN
再次强调第一条黄金定律,减少网页内容的下载时间。提高下载速度还可以通过CDN(内容分发网络)来提升。CDN通过部署在不同地区的服务器来提高客户的下载速度。如果你的网站上有大量的静态内容,世界各地的用户都在访问,我说的是youtube么?那CDN是必不可少的。事实上大多数互联网中的巨头们都有自己的CDN。我们自己的网站可以先通过免费的CDN供应商来分发网页资源。
添加Expires 或Cache-Control报文头
这条规则分为两个方面,
- 对于静态内容添加Expires,将静态内容设为永不过期,或者很长时间以后。在IIS中设置Expires可以看Configure the HTTP Expires Response Header (IIS 7)。
- 对于动态内容应用合适的Cache-Control,让浏览器根据条件来发送请求。关于asp.net的caching,可以看asp.net cache feature和asp.net caching best practices。
Gzip压缩传输文件
Gzip通常可以减少70%网页内容的大小,包括脚本、样式表、图片等文件。Gzip比deflate更高效,主流服务器都有相应的压缩支持模块。
IIS中内建了静态压缩和动态压缩模块,如何配制可以参考Enable HTTP Compression of Static Content (IIS 7)和Enable HTTP Compression of Dynamic Content (IIS 7)。
值得注意的是pdf文件可以从需要被压缩的类型中剔除,因为pdf文件本身已经压缩,gzip对其效果不大,而且会浪费CPU。
配置ETags
虽然标题叫配制ETags,但是这里你要根据具体情况进行一些判断。首先Etag简单来说是通过一个文件版本标识使得服务器可以轻松判断该请求的内容是否有所更新,如果没有就回复304 (not modified),从而避免下载整个文件。
但是Etags的版本信息即使主流服务器未能很好地支持跨服务器的判断,比如你从一个服务器集群中一台得到Etags,然后发送到了另一台那么校验很有可能会失败。
如果你遇到这样的问题,IIS 7中可以通过如下方法将Etag去掉,使用URL Rewrite,然后在web.config中添加如下配制
<rewrite>
<outboundRules>
<rule name="Remove ETag">
<match serverVariable="RESPONSE_ETag" pattern=".+" />
<action type="Rewrite" value="" />
</rule>
</outboundRules>
</rewrite>
IIS8里提供了一个简单配制来直接关闭Etag,
<element name="clientCache"><attribute name="setEtag" type="bool" defaultValue="false" />
<attribute name="cacheControlMode" type="enum" defaultValue="NoControl">
<enum name="NoControl" value="0" />
<enum name="DisableCache" value="1" />
<enum name="UseMaxAge" value="2" />
<enum name="UseExpires" value="3" />
</attribute>
<attribute name="cacheControlMaxAge" type="timeSpan" defaultValue="1.00:00:00" />
<attribute name="httpExpires" type="string" />
<attribute name="cacheControlCustom" type="string" />
</element>
尽早flush输出
网页后台程序中我们知道有个方法叫Response.Flush(),一般我们调用它都是在程序末尾,但注意这个方法可以被调用多次。目的是可以将现有的缓存中的回复内容先发给客户端,让客户端“有活干”。
那在什么时候调用这个方法比较好呢?一般情况下我们可以在对于需要加载比较多外部脚本或者样式表时可以提前调用一次,客户端收到了关于脚本或其他外部资源的链接可以并行的先发请求去下载,服务器接下来把后续的处理结果发给客户端。
使用GET Ajax请求
浏览器在实现XMLHttpRequest POST的时候分成两步,先发header,然后发送数据。而GET却可以用一个TCP报文完成请求。另外GET从语义上来讲是去服务器取数据,而POST则是向服务器发送数据,所以我们使用Ajax请求数据的时候尽量通过GET来完成。
关于GET和POST的详细对比可以查看这里。
避免空的图片src
空的图片src仍然会使浏览器发送请求到服务器,这样完全是浪费时间,而且浪费服务器的资源。尤其是你的网站每天被很多人访问的时候,这种空请求造成的伤害不容忽略。
浏览器如此实现也是根据RFC 3986 - Uniform Resource Identifiers标准,空的src被定义为当前页面。
所以注意我们的网页中是否存在这样的代码
straight HTML
<img src="">
JavaScript
var img = new Image();
img.src = "";
Cookie
减少Cookie大小
Cookie被用来做认证或个性化设置,其信息被包含在http报文头中,对于cookie我们要注意以下几点,来提高请求的响应速度,
- 去除没有必要的cookie,如果网页不需要cookie就完全禁掉
- 将cookie的大小减到最小
- 注意cookie设置的domain级别,没有必要情况下不要影响到sub-domain
- 设置合适的过期时间,比较长的过期时间可以提高响应速度。
关于asp.net中的cookie可以参考ASP.NET Cookies Overview和Configure Use Cookies Mode for Session State (IIS 7)
页面内容使用无cookie域名
大多数网站的静态资源都没必要cookie,我们可以采用不同的domain来单独存放这些静态文件,这样做不仅可以减少cookie大小从而提高响应速度,还有一个好处是有些proxy拒绝缓存带有cookie的内容,如果能将这些静态资源cookie去除,那就可以得到这些proxy的缓存支持。
常见的划分domain的方式是将静态文件放在static.example.com,动态内容放在www.example.com。
也有一些网站需要在二级域名上应用cookie,所有的子域都会继承,这种情况下一般会再购买一个专门的域名来存放cookie-free的静态资源。例如Yahoo!的yimg.com,YouTube的ytimg.com等。
CSS
将样式表置顶
经样式表(css)放在网页的HEAD中会让网页显得加载速度更快,因为这样做可以使浏览器逐步加载已将下载的网页内容。这对内容比较多的网页尤其重要,用户不用一直等待在一个白屏上,而是可以先看已经下载的内容。
如果将样式表放在底部,浏览器会拒绝渲染已经下载的网页,因为大多数浏览器在实现时都努力避免重绘,样式表中的内容是绘制网页的关键信息,没有下载下来之前只好对不起观众了。
避免CSS表达式
CSS表达式可以动态的设置CSS属性,在IE5-IE8中支持,其他浏览器中表达式会被忽略。例如下面表达式在不同时间设置不同的背景颜色。
background-color: expression( (new Date()).getHours()%2 ? "#B8D4FF" : "#F08A00" );
CSS表达式的问题在于它被重新计算的次数远比我们想象的要多,不仅在网页绘制或大小改变时计算,即使我们滚动屏幕或者移动鼠标的时候也在计算,因此我们还是尽量避免使用它来防止使用不当而造成的性能损耗。
如果想达到类似的效果我们可以通过简单的脚本做到。
<html>
<head>
</head>
<body>
<script type="text/javascript">
var currentTime = new Date().getHours();
if (currentTime%2) {
if (document.body) {
document.body.style.background = "#B8D4FF";
}
}
else {
if (document.body) {
document.body.style.background = "#F08A00";
}
}
</script>
</body>
</html>
用<link>代替@import
避免使用@import的原因很简单,因为它相当于将css放在网页内容底部。
避免使用Filters
AlphaImageLoad也是IE5.5 - IE8中支持,这种滤镜的使用会导致图片在下载的时候阻塞网页绘制,另外使用这种滤镜会导致内存使用量的问题。IE9中已经不再支持。
Javascript
将脚本置底
HTTP/1.1 specification建议浏览器对同一个hostname不要超过两个并行下载连接, 所以当你从多个domain下载图片的时候可以提高并行下载连接数量。但是当脚本在下载的时候,即使是来自不同的hostname浏览器也不会下载其他资源,因为浏览器要在脚本下载之后依次解析和执行。
因此对于脚本提速,我们可以考虑以下方式,
使用外部Javascirpt和CSS文件
使用外部Javascript和CSS文件可以使这些文件被浏览器缓存,从而在不同的请求内容之间重用。
同时将Javascript和CSS从inline变为external也减小了网页内容的大小。
使用外部Javascript和CSS文件的决定因素在于这些外部文件的重用率,如果用户在浏览我们的页面时会访问多次相同页面或者可以重用脚本的不同页面,那么外部文件形式可以为你带来很大的好处。但对于用户通常只会访问一次的页面,例如microsoft.com首页,那inline的javascript和css相对来说可以提供更高的效率。
精简Javascript和CSS
精简就是将Javascript或CSS中的空格和注释全去掉,
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
精简后版本
body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}
统计表明精简后的文件大小平均减少了21%,即使在应用Gzip的文件也会减少5%。
例如我的网站上有5个CSS,4个Javascirpt,下面是分别经过bundling和minify之后的结果。
没有任何处理之前 | 捆绑Javascript和CSS之后 | 精简Javascript和CSS之后 |
用来帮助我们做精简的工具很多,主要可以参考如下,
JS compressors:
- Packer
- JSMin
- Closure compiler
-
YUICompressor (also does CSS)
- AjaxMin (also does CSS)
CSS compressors:
- CSSTidy
- Minify
-
YUICompressor (also does JS)
- AjaxMin (also does JS)
- CSSCompressor
与VS集成比较好的工具如下.
- YUICompressor - 编译集成,包含在NuGet.
- AjaxMin - 编译集成
去除重复脚本
重复的脚本不仅浪费浏览器的下载时间,而且浪费解析和执行时间。一般用来避免引入重复脚本的做法是使用统一的脚本管理模块,这样不仅可以避免重复脚本引入,还可以兼顾脚本依赖管理和版本管理。
减少DOM访问
通过Javascript访问DOM元素没有我们想象中快,元素多的网页尤其慢,对于Javascript对DOM的访问我们要注意
- 缓存已经访问过的元素
- Offline更新节点然后再加回DOM Tree
- 避免通过Javascript修复layout
使用智能事件处理
这里说智能的事件处理需要开发者对事件处理有更深入的了解,通过不同的方式尽量少去触发事件,如果必要就尽早的去处理事件。
比如一个div中10个按钮都需要事件句柄,那么我们可以将事件放在div上,在事件冒泡过程中捕获该事件然后判断事件来源。
图片
优化图像
当美工完成了网站的图片设计后,我们可以在上传图片之前对其做以下优化
- 检查GIF图片中图像颜色的数量是否和调色板规格一致。如果你发现图片中只用到了4种颜色,而在调色板的中显示的256色的颜色槽,那么这张图片就还有压缩的空间。可以使用imagemagick检查:
identify -verbose image.gif - 尝试把GIF格式转换成PNG格式,看看是否节省空间。大多数情况下是可以压缩的。下面这条简单的命令可以安全地把GIF格式转换为PNG格式:
convert image.gif image.png - 在所有的PNG图片上运行pngcrush(或者其它PNG优化工具)。例如:
pngcrush image.png -rem alla -reduce -brute result.png - 在所有的JPEG图片上运行jpegtran。这个工具可以对图片中的出现的锯齿等做无损操作,同时它还可以用于优化和清除图片中的注释以及其它无用信息
jpegtran -copy none -optimize -perfect src.jpg dest.jpg
优化CSS Sprite
- Spirite中水平排列图片,垂直排列会增加文件大小;
- Spirite中把颜色较近的组合在一起可以降低颜色数,理想状况是低于256色以便适用PNG8格式;
- 不要在Spirite的图像中间留有较大空隙。这虽然不大会增加文件大小,但对于用户代理来说它需要更少的内存来把图片解压为像素地图。100×100的图片为1万像素,1000×1000就是100万像素。
不要在HTML中缩放图片
不要通过图片缩放来适应页面,如果你需要小图片,就直接使用小图片吧。
使用小且可缓存的favicon.ico
网站图标文件favicon.ico,不管你服务器有还是没有,浏览器都会去尝试请求这个图标。所以我们要确保这个图标
- 存在
- 文件尽量小,最好小于1k
- 设置一个长的过期时间
移动客户端
保持单个内容小于25KB
这限制是因为iphone,他只能缓存小于25K,注意这是解压后的大小。所以单纯gzip不一定够用,精简文件工具要用上了。
打包组建成符合文档
把页面内容打包成复合文本就如同带有多附件的Email,它能够使你在一个HTTP请求中取得多个组建。当你使用这条规则时,首先要确定用户代理是否支持(iPhone不支持)。
JavaScript性能优化
如今主流浏览器都在比拼JavaScript引擎的执行速度,但最终都会达到一个理论极限,即无限接近编译后程序执行速度。 这种情况下决定程序速度的另一个重要因素就是代码本身。
在这里我们会分门别类的介绍JavaScript性能优化的技巧,并提供相应的测试用例,供大家在自己使用的浏览器上验证, 同时会对特定的JavaScript背景知识做一定的介绍。
目录
变量查找优化
变量声明带上var
1. 如果声明变量忘记了var,那么js引擎将会遍历整个作用域查找这个变量,结果不管找到与否,都是悲剧。
- 如果在上级作用域找到了这个变量,上级作用域变量的内容将被无声的改写,导致莫名奇妙的错误发生。
- 如果在上级作用域没有找到该变量,这个变量将自动被声明为全局变量,然而却都找不到这个全局变量的定义。
2. 基于上面逻辑,性能方面不带var声明变量自然要比带var速度慢
具体可以参考http://jsperf.com/withvar-withoutvar。下面是个简单的结果截图,蓝色为带var的情况,越长说明 速度越快。
慎用全局变量
1. 全局变量需要搜索更长的作用域链。
2. 全局变量的生命周期比局部变量长,不利于内存释放。
3. 过多的全局变量容易造成混淆,增大产生bug的可能性。
全局变量与局部变量的测试可以参考http://jsperf.com/local-global-var
以上两条还可以得出一条JavaScript常用的编程风格,具有相同作用域变量通过一个var声明 。
这样方便查看该作用域所有的变量,JQuery源代码中就是用了这种风格。例如下面源代码
https://github.com/jquery/jquery/blob/master/src/core.js
1234 | jQuery.extend function () { var arguments.length,deep false ; |
缓存重复使用的全局变量
1. 全局变量要比局部变量需要搜索的作用域长
2. 重复调用的方法也可以通过局部缓存来提速
3. 该项优化在IE上体现比较明显
缓存与不缓存变量的测试可以参考http://jsperf.com/localvarcache
JQuery源代码中也是用了类似的方法,https://github.com/jquery/jquery/blob/master/src/selector-native.js
12345678910 | var
matches docElem.msMatchesSelector, selector_sortOrder function ( a, b ) { // if
selector_hasDuplicate = true ; return
} |
避免使用with
with语句将一个新的可变对象推入作用域链的头部,函数的所有局部变量现在处于第二个作用域链对象中,从而使局部变 量的访问代价提高。
1234567891011 | var
name: “Nicholas", age: 30 } function
var
with
alert(name + ' is ' + age); alert( 'count is ' + count); } } |
以上代码的结果将name和age两个变量推入第一个作用域,如下图所示,
使用with与不使用with的测试可以参考http://jsperf.com/with-with
核心语法优化
通过原型优化方法定义
1. 如果一个方法类型将被频繁构造,通过方法原型从外面定义附加方法,从而避免方法的重复定义。
2. 可以通过外 部原型的构造方式初始化值类型的变量定义。(这里强调值类型的原因是,引用类型如果在原型中定义, 一个实例对引用类型的更改会影响到其他实例。)
这条规则中涉及到JavaScript中原型的概念,
- 构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。我们可 以把那些不变的属性和方法,直接定义在prototype对象上。
- 可以通过对象实例访问保存在原型中的值,不能通过对象实例重写原型中的值。
- 在实例中添加一个与实例原型同名属性,那该属性就会屏蔽原型中的属性。
- 通过delete操作符可以删除实例中的属性。
例如以下代码以及相应的内存中原型表示如下,
1234567891011 | function
Person.prototype.name "Nicholas" ; Person.prototype.age Person.prototype.job "Software Engineer" ; Person.prototype.sayName function (){ alert( this .name); }; var
new
person1.sayName(); //”Nicholas” var
new
person2.sayName(); //”Nicholas” |
原型附加方法测试可以参考http://jsperf.com/func-constructor
原型附加值类型变量测试可以参考http://jsperf.com/prototype2
避开闭包陷阱
1. 闭包是个强大的工具,但同时也是性能问题的主要诱因之一。不合理的使用闭包会导致内存泄漏。
2. 闭包的性能不如使用内部方法,更不如重用外部方法。
由于IE浏览器的DOM是用COM来实现的, COM的内存管理是通过引用计数的方式,引用计数有个难题就是循环引用,一旦DOM 引用了闭包(例如event handler),闭包的上层元素又引用了这个DOM,就会造成循环引用从而导致内存泄漏。
关于Js内存泄漏可以参考
http://www.crockford.com/javascript/memory/leak.html
http://msdn.microsoft.com/en-us/library/bb250448%28v=vs.85%29.aspx
闭包与非闭包的测试http://jsperf.com/closure2
避免使用属性访问方法
1. JavaScript不需要属性访问方法,因为所有的属性都是外部可见的。
2. 添加属性访问方法只是增加了一层重定向 ,对于访问控制没有意义。
使用属性访问方法示例
1234567891011121314151617 | function
this .m_tireSize = 17; this .m_maxSpeed = 250; this .GetTireSize = Car_get_tireSize; this .SetTireSize = Car_put_tireSize; } function
return
.m_tireSize; } function
this .m_tireSize = value; } var
new
var
ooCar.SetTireSize(iTireSize |
直接访问属性示例
1234567 | function
this .m_tireSize = 17; this .m_maxSpeed = 250; } var
new
var
perfCar.m_tireSize |
使用属性访问与不使用属性访问的测试http://jsperf.com/property-accessor
避免在循环中使用try-catch
1. try-catch-finally语句在catch语句被执行的过程中会动态构造变量插入到当前域中,对性能有一定影响。
2. 如 果需要异常处理机制,可以将其放在循环外层使用。
循环中使用try-catch
123 | for
var
try
catch
} |
循环外使用try-catch
123 | try
for
var
} catch
|
循环内与循环外使用try-catch的测试http://jsperf.com/try-catch
使用for代替for…in…遍历数组
for…in…内部实现是构造一个所有元素的列表,包括array继承的属性,然后再开始循环。相对for循环性能要慢。
*上对这个for和for in的问题有个经典的回答,直接原文引用,
Q: I've been told not to use "for...in" with arrays in JavaScript. Why not?
A: The reason is that one construct...
123456 | <code> var
a[5] // Perfectly legal JavaScript that resizes the array. for
var
// Iterates over numeric indexes from 0 to 5, as everyone expects. }</code> |
can sometimes be totally different from the other...
12345 | <code> var
a[5] for
var
in
// Shows only the explicitly set index of "5", and ignores 0-4 }</code> |
Also consider that JavaScript libraries might do things like this, which will affect any array you create:
1234567891011 | <code> // Somewhere deep in your Array.prototype.foo // var
for
var
in
// Now foo is a part of EVERY array and // will show up here as a value of 'x'. }</code> |
关于for和for…in…的测试可以看http://jsperf.com/forin
使用原始操作代替方法调用
方法调用一般封装了原始操作,在性能要求高的逻辑中,可以使用原始操作代替方法调用来提高性能。
原始操作
1 | var
|
方法实例
1 | var
|
关于方法调用和原始操作的测试参考http://jsperf.com/operator-function
传递方法取代方法字符串
一些方法例如setTimeout()/setInterval(),接受字符串或者方法实例作为参数。直接传递方法对象作为参数来避免对字 符串的二次解析。
传递方法
1 | setTimeout(test, |
传递方法字符串
1 | setTimeout( 'test()' , 1); |
对应的测试可以参考http://jsperf.com/string-function
脚本装载优化
使用工具精简脚本
精简代码就是将代码中的空格和注释去除,也有更进一步的会对变量名称混淆+精简。
根据统计精简后文件大小平均减少21%,即使Gzip之后文件也会减少5%。
常用的工具如下,
例如Closure Compiler效果如下,
启用Gzip压缩
Gzip通常可以减少70%网页内容的大小,包括脚本、样式表、图片等文件。Gzip比deflate更高效,主流服务器都有相应的 压缩支持模块。
Gzip的工作流程为
- 客户端在请求Accept-Encoding中声明可以支持gzip
- 服务器将请求文档压缩,并在Content-Encoding中声明该回复为gzip格式
- 客户端收到之后按照gzip解压缩
设置Cache-Control和Expires头
通过Cache-Control和Expires头可以将脚本文件缓存在客户端或者代理服务器上,可以减少脚本下载的时间。
123456789 | Expires格式: Expires Expires: Note: Expires Cache-Control格式: Cache-Control Cache-Control: |
具体的标准定义可以参考http1.1中的定义,简单来说Expires控制过期时间是多久,Cache-Control控制什么地方可以缓存 。
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
异步加载脚本
脚本加载与解析会阻塞HTML渲染,可以通过异步加载方式来避免渲染阻塞。
异步加载的方式很多,比较通用的方法是通过类似下面的代码实现,
12345678910111213141516 | function (script_filename){ var
'script' ); script.setAttribute( 'type' , 'text/javascript' ); script.setAttribute( 'src' , script_filename); script.setAttribute( 'id' , 'script-id' ); scriptElement = document.getElementById( 'script-id' ); if (scriptElement){ document.getElementsByTagName( 'head' )[0].removeChild(scriptElement); } document.getElementsByTagName( 'head' )[0].appendChild(script); } var
'scripts/alert.js' ; loadjs(script); |
DOM操作优化
DOM操作性能问题主要有以下原因,
- DOM元素过多导致元素定位缓慢
- 大量的DOM接口调用
- DOM操作触发频繁的reflow(layout)和repaint
关于reflow(layout)和repaint可以参考下图,可以看到layout发生在repaint之前,所以layout相对来说会造成更多性能 损耗。
- reflow(layout)就是计算页面元素的几何信息
- repaint就是绘制页面元素
以下是一个wikipedia网站reflow的过程录像,
减少DOM元素数量
1. 在console中执行命令查看DOM元素数量
1 | document.getElementsByTagName( '*' ).length |
2. Yahoo首页DOM元素数量在1200左右。正常页面大小一般不应该超过 1000。
3. DOM元素过多会使DOM元素查询效率,样式表匹配效率降低,是页面性能最主要的瓶颈之一。
优化CSS样式转换
如果需要动态更改CSS样式,尽量采用触发reflow次数较少的方式。
例如以下代码逐条更改元素的几何属性,理论上会触发多次reflow
123 | element.style.fontWeight 'bold' ; element.style.marginLeft= '30px' ; element.style.marginRight '30px' ; |
可以通过直接设置元素的className直接设置,只会触发一次reflow
123 | element.className 'selectedAnchor' ; |
具体的测试结果如下,
测试用例可以参考http://jsperf.com/css-class
优化节点添加
多个节点插入操作,即使在外面设置节点的元素和风格再插入,由于多个节点还是会引发多次reflow。优化的方法是创建 DocumentFragment,在其中插入节点后再添加到页面。
例如JQuery中所有的添加节点的操作如append,都是最终调用documentFragment来实现的,
http://code.jquery.com/jquery-1.10.2.js
123456789101112131415 | function createSafeFragment( var
"|"
safeFrag = document.createDocumentFragment(); if
while
safeFrag.createElement( list.pop() ); } } return
} |
关于documentFragment对比直接添加节点的测试http://jsperf.com/fragment2
优化节点修改
对于节点的修改,可以考虑使用cloneNode在外部更新节点然后再通过replace与原始节点互换。
123456789 | var
'container' ); var
true ); var
'foo' , 'bar' , 'baz' ]; var
for
var
content = document.createTextNode(list[i]); clone.appendChild(content); } orig.parentNode.replaceChild(clone, |
对应的测试可以参考http://jsperf.com/clone-node2
减少使用元素位置操作
一般浏览器都会使用增量reflow的方式将需要reflow的操作积累到一定程度然后再一起触发,但是如果脚本中要获取以下 属性,那么积累的reflow将会马上执行,已得到准确的位置信息。
- offsetLeft
- offsetTop
- offsetHeight
- offsetWidth
- scrollTop/Left/Width/Height
- clientTop/Left/Width/Height
- getComputedStyle()
避免遍历大量元素
避免对全局DOM元素进行遍历,如果parent已知可以指定parent在特定范围查询。
例如以下示例,
1234 | var
'*' ); for
if
'selected' )) {} } |
如果已知元素存在于一个较小的范围内,
123456 | var ( 'canvas' ).getElementsByTagName( '*' ); for
if
'selected' )) {} } |
1 |
相关测试可以参考http://jsperf.com/ranged-loop
事件优化
使用事件代理
1. 当存在多个元素需要注册事件时,在每个元素上绑定事件本身就会对性能有一定损耗。
2. 由于DOM Level2事件模 型中所有事件默认会传播到上层文档对象,可以借助这个机制在上层元素注册一个统一事件对不同子元素进行相应处理。
捕获型事件先发生。两种事件流会触发DOM中的所有对象,从document对象开始,也在document对象结束。
http://www.w3.org/TR/2003/NOTE-DOM-Level-3-Events-20031107/events.html
示例代码如下
12345678 | < ul
< li
< li
< li
< li
< li
< li
</ li ></ ul > |
123456789 | // document.getElementById( "parent-list" ).addEventListener( "click" , function (e) { // e.target is the clicked element! // If it was a list item if (e.target && e.target.nodeName == "LI" ) { // List item found! Output the ID! console.log( "List item " ,e.target.id.replace( "post-" ), " was clicked!" ); } }); |
对应的测试可以参考http://jsperf.com/event- delegate
动画优化
动画效果在缺少硬件加速支持的情况下反应缓慢,例如手机客户端
特效应该只在确实能改善用户体验时才使用,而不应用于炫耀或者弥补功能与可用性上的缺陷
至少要给用户一个选择可以禁用动画效果
设置动画元素为absolute或fixed
position: static 或position: relative元素应用动画效果会造成频繁的reflow
position: absolute或position: fixed 的元素应用动画效果只需要repaint
关于position的具体介绍可以参考
http://css- tricks.com/almanac/properties/p/position/
使用一个timer完成多个元素动画
setInterval和setTimeout是两个常用的实现动画的接口,用以间隔更新元素的风格与布局。
动画效果的帧率最优化的情况是使用一个timer完成多个对象的动画效果,其原因在于多个timer的调用本身就会损耗一定 性能。
123456 | setInterval( function () { animateFirst( '' ); }, setInterval( function () { animateSecond( '' ); }, |
使用同一个timer,
1234 | setInterval( function () { animateFirst( '' ); animateSecond( '' ); }, |
以上是JavaScript性能提高的技巧总结,基本上都能够通过测试验证,但是限于篇幅没有把所有的测试结果都 贴出来。