前端性能和优化
简介
关于前端性能和优化我们有雅虎14条性能优化原则,还有两本很经典的性能优化指导书:《高性能网站建设指南》、《高性能网站建设进阶指南》。经验丰富的工程师对于前端性能优化方法耳濡目染,基本都能一一列举出来。这些性能优化原则大概是在7年前提出的,对于web性能优化至今都有非常重要的指导意义。
雅虎性能优化详细描述
1.减少http请求
分类: 内容
终端用户有80%的响应时间是花费在前端,大部分时间用于下载页面中的所有组件:图像、样式表、脚本、Flash等。减少组件数量反过来说就是减少呈现页面所需的HTTP请求数量。这是让页面速度更快的关键。
减少页面中组件数量的一种方法是简化页面的设计。但是,是否有一种方法可以在构建内容更丰富的页面的同时实现快速的响应时间呢?下面是一些在支持丰富页面设计的同时减少HTTP请求数量的技术。
组合文件是一种减少HTTP请求数量的方法,方法是将所有脚本组合到一个脚本中,类似地将所有CSS组合到一个样式表中。当脚本和样式表随着页面的变化而变化时,组合文件就更具有挑战性了,但是发布过程中做到这点能够提高响应时间。
CSS Sprites(精灵)是减少图像请求数量的首选方法。将网页中的背景图像合并为一张图片中,再利用CSS的“background-image”,“background-position”来显示所需的图像片段。
图像映射将多个图像合并成一个图像。整体大小大致相同,但是减少HTTP请求的数量会加快页面的速度。图像映射只有在图像在页面中连续时才有效,比如导航栏。定义图像映射的热点可能是乏味的和容易出错的。使用图像映射作为导航也是不可访问的,所以不推荐使用。
内嵌图像使用data: URL模式将图像数据嵌入到实际页面中。这会增加HTML文档的大小。将内联图像合并到(缓存的)样式表中是减少HTTP请求和避免增加页面大小的一种方法。目前内嵌图像说有主流浏览器还不完全还不支持
减少页面中HTTP请求的数量是个起点。这是提高首次访问者性能的最重要指南。正如在Tenni Theurer的博客文章中所描述的那样,40%到60%的访客在访问你的站点时,缓存都是空的。让这些首次访问者快速访问页面是获得更好用户体验的关键。
2.使用CDN(Content Delivery Network)
分类: 服务器
用户与服务器的物理距离对响应时间也有影响。把内容部署在多个地理位置分散的服务器上能让用户更快地载入页面。但具体要怎么做呢?
实现内容在地理位置上分散的第一步是:不要尝试去重新设计你的web应用程序来适应分布式结构。根据应用程序的不同,更改体系结构可能包括一些艰巨的任务,比如同步会话状态和跨服务器位置复制数据库事务。此应用程序架构步骤中尝试缩短用户与您的内容之间的距离可能会延迟或永远不会通过。
请记住,80-90%的最终用户响应时间用于下载页面中的所有组件:图像、样式表、脚本、Flash等等。与其从重新设计应用程序体系结构的困难任务开始,不如先分散静态内容。这不仅大大缩短了响应时间,而且由于有了内容分发网络,这也更容易了。
内容分发网络(CDN)是一组分散在不同地理位置的web服务器,用来给用户更高效地发送内容。典型地,选择用来发送内容的服务器是基于网络距离的衡量标准的。例如:选跳数(hop)最少的或者响应时间最快的服务器。
一些大型互联网公司拥有自己的CDN,但使用CDN服务提供商(如Akamai Technologies、EdgeCast或level3)具有成本效益。对于初创公司和私有web站点,CDN服务的成本可能高得令人望而却步,但随着目标受众的增长和全球化程度的提高,CDN对于实现快速响应时间是必要的。在Yahoo!中,将静态内容从其应用程序Web服务器移动到CDN(如上所述的第三方以及Yahoo自己的CDN)的做法将终端用户响应时间提高了20%或更多。切换到CDN是一个相对容易的代码更改,它将极大地提高web站点的速度。
3.添上Expires或者Cache-Control HTTP头
分类: 服务器
这条规则有两个方面:
对于静态组件:通过设置一个遥远的将来时间作为Expires来实现永不失效
对于动态组件:用合适的Cache-ControlHTTP头来让浏览器进行条件性的请求
Web页面设计越来越丰富,这意味着页面中有更多的脚本、样式表、图像和Flash。第一次访问您页面的访问者可能需要发出几个HTTP请求,但是通过使用Expires头,您可以使这些组件可缓存。这避免了在接下来的浏览过程中不必要的HTTP请求。Expires头通常被用在图片上,但它们应该用在所有组件上,包括脚本、样式和Flash组件。
前端性能优化:Add Expires headers
浏览器(和代理)使用缓存来减少HTTP请求的数量和大小,使web页面加载速度更快。web服务器在HTTP响应中使用Expires头告诉客户端一个组件可以缓存多长时间。这是一个未来的Expires头,告诉浏览器这个响应在2010年4月15日之前不会失效。
Expires: Thu, 15 Apr 2010 20:00:00 GMT
如果您的服务器是Apache,使用ExpiresDefault指令来设置一个相对于当前日期的过期日期。下面的例子设置了从请求时间起10年的有效期:
ExpiresDefault "access plus 10 years"
记住,如果你用一个遥远的未来时间做有效期,就不得不在组件发生变化后及时修改组件的文件名。在Yahoo!,我们经常把这一步作为构建过程的一部分:把版本号内嵌在组件的文件名里,例如:yahoo_2.0.6.js
用一个遥远的未来时间做有效期HTTP头,只有在用户已经访问过站点之后才会影响页面视图。如果是新访客或者浏览器的缓存被清空时,对HTTP请求的数量并没有影响。因此这种性能提升取决于已缓存各个组件的用户访问站点的频率。我们在Yahoo!测量了这个数据,发现已缓存各个组件的页面访问量(PV)占75%到85%。通过把一个遥远的未来时间作为有效期HTTP头,增加了被浏览器缓存的组件数量,在后续页面访问量中不需要用Internet连接多发送哪怕一个字节。
4.Gzip组件
分类: 服务器
通过网络传输HTTP请求和响应所需的时间可以通过前端工程师的决策大大减少。的确,终端用户的带宽速度、互联网服务提供商、靠近对等交换点等都超出了开发团队的控制范围。但还有别的能够影响响应时间的因素,压缩可以通过减少HTTP响应的大小来缩短响应时间。
从HTTP/1.1开始,web客户端就有了支持压缩的Accept-Encoding HTTP请求头。
Accept-Encoding: gzip, deflate
如果web服务器看到这个请求头,它就会用客户端列出的一种方式来压缩响应。web服务器通过Content-Encoding相应头来通知客户端。
Content-Encoding: gzip
Gzip是目前最常见的高效压缩方法,由GNU项目开发并被RFC 1952标准化。唯一一个你可能会看到的其它压缩格式是deflate,但它效率不高而且并不常见。
Gzipping一般能够把响应压缩到70%左右,目前大约90%的通过浏览器的网络传输都支持gzip。如果是Apache服务器,配置gzip的模块取决于版本:Apache 1.3用mod_gzip而Apache 2.x是mod_deflate模块。
浏览器和代理的某些因素可能会引起浏览器所期望的和它收到的压缩内容不匹配。幸运的是,随着老旧浏览器的淘汰,这些极少遇到的情况正在逐渐减少,而且Apache模块可以通过自动添加合适的Vary响应头来帮你搞定。
服务器会根据文件类型来决定要不要用gzip压缩,但这非常有限。大多数网站都用gzip压缩HTML文件,其实压缩脚本,样式表也是不错的选择,但很多网站却错失了这个机会。其实,可以压缩任何文本内容,包括XML和JSON,而图片和PDF不用压缩,因为它们已经被压缩过了,再用gzip压缩不仅浪费CPU还可能会越压越大。
尽可能多地用gzip压缩能够给页面减肥,这也是提升用户体验最简单的方法。
5.把样式表放在顶部
分类: css
在Yahoo!研究性能的时候,我们发现把样式表放到文档的HEAD部分能让页面看起来加载地更快。这是因为把样式表放在head里能让页面逐步渲染。
关注性能的前端工程师想让页面逐步渲染。也就是说,我们想让浏览器尽快显示已有内容,这在页面上有一大堆内容或者用户网速很慢时显得尤为重要。给用户显示反馈(比如进度指标)的重要性已经被广泛研究过,并且被记录下来了。在我们的例子中,HTML页面就是进度指标!当浏览器逐渐加载页面头部,导航条,顶部logo等等内容的时候,这些都被正在等待页面加载的用户当作反馈,能够提高整体用户体验。
在很多浏览器(包括IE)中,把样式表放在HTML文档底部都会阻止页面逐渐渲染。这些浏览器阻塞渲染过程,以避免因为样式变动而重绘页面元素,用户这时就只能盯着空白页面。
HTML官方文档清楚地描述了样式表应该放在页面的HEAD里面:”Unlike A, [LINK] may only appear in the HEAD section of a document, although it may appear any number of times.”(不像a标签,link标签可能只出现在HEAD部分,虽然它能可以出现任意多次)。空白屏幕或者没有样式的falsh内容都是不可取的。理想方案就是遵循HTML官方文档,把样式表放在HTML文档的HEAD部分。
6.把脚本放在底部
分类: javascript
脚本会阻塞并行下载,HTTP/1.1官方文档建议浏览器每个主机名下并行下载的组件数不要超过两个,如果图片来自多个主机名,并行下载的数量就可以超过两个。如果脚本正在下载,浏览器就不开始任何其它下载任务,即使是在不同主机名下的。
有时候,并不容易把脚本移动到底部。举个例子,如果脚本是用document.write
插入到页面内容中的,就没办法再往下移了。还可能存在作用域问题,在多数情况下,这些问题都是可以解决的。
一个常见的建议是用推迟(deferred)脚本,有DEFER
属性的脚本意味着不能含有document.write,并且提示浏览器告诉他们可以继续渲染。不幸的是,Firefox不支持DEFER
属性。在IE中,脚本可能被推迟,但不尽如人意。如果脚本可以推迟,我们就可以把它放到页面底部,页面就可以更快地载入。
7.避免使用CSS表达式
分类: css
用CSS表达式动态设置CSS属性,是一种强大又危险的方式。从IE5开始支持,但从IE8起就不推荐使用了。例如,可以用CSS表达式把背景颜色设置成按小时交替的:
background-color: expression( (new Date()).getHours()%2 ? "#B8D4FF" : "#F08A00" );
上面的代码中,expression方法可以接受一个JavaScript表达式。CSS属性会被设置成表达式的计算结果。expression方法会被其它浏览器忽略,所以只有想办法实现跨浏览器的与IE一致的用户体验才有用。
表达式最大的问题是它们经常被重复计算,比我们想象的次数还要多。不仅仅是页面渲染和调整大小的时候,在页面被滚动,甚至用户在页面上移动鼠标时都会重新计算表达式。给CSS表达式添加一个计数器就可以追踪它重新计算的时间和频率,而在页面上动动鼠标就可以引发10000多次重新计算。
减少CSS表达式重新计算的一种方式就是用一次性表达式
,即在表达式第一次计算后就把样式属性设置成一个明确的值,换掉表达式。如果必须要在页面的整个生命周期中动态设置样式属性,可以用事件处理器来代替CSS表达式。如果必须使用CSS表达式,要记得它们可能会被重复计算上千次,从而影响整个页面的性能。
8.把JavaScript和CSS放到外面
分类: javascript, css
很多性能原则都是关于如何管理外部组件的,然而,在这些顾虑出现之前你应该问一个更基础的问题:应该把JavaScript和CSS放到外部文件中还是直接写在页面里?
实际上,用外部文件可以让页面更快,因为JavaScript和CSS文件会被缓存在浏览器。HTML文档中的行内JavaScript和CSS在每次请求该HTML文档的时候都会重新下载。这样做减少了所需的HTTP请求数,但增加了HTML文档的大小。另一方面,如果JavaScript和CSS在外部文件中,并且已经被浏览器缓存起来了,那么我们就成功地把HTML文档变小了,而且还没有增加HTTP请求数。
关键因素是,外部文件被缓存的频率和页面被请求数量之间的关系。尽管这个因素很难量化,但我们还是可以用各种各样的指标来衡量。如果用户的每个会话中都有多次页面访问,那么相同的脚本和样式表就可以被多个页面复用,缓存的外部文件就会带来巨大的好处。
很多站点在度量中都处于中等水平,对这些站点来说,一般最好的解决方案就是把JavaScript和CSS部署为外部文件。唯一的例外是主页上行内方式优先,例如Yahoo!的首页和My Yahoo!。在每个会话中访问量比较少的主页会发现行内JavaScript和CSS能让终端用户的响应时间更快。
对典型的站点来说,首页是众多访问量的开始,有很多技术可以对减少HTTP请求起到杠杆作用,就像用外部文件缓存的好处一样。这样的一种技术就是在首页用行内JavaScript和CSS
,但在页面载入完成之后动态加载外部文件
,这样后续的页面所需的外部文件就已经被放到浏览器的缓存里了。
9.减少DNS查找
分类: 内容
域名系统 (DNS) 建立了主机名和IP地址间的映射,就像电话簿上人名和号码的映射一样。当你在浏览器输入www.yahoo.com的时候,浏览器就会联系DNS解析器返回服务器的IP地址。DNS是有成本的,它需要20到120毫秒去查找给定主机名的IP地址。在DNS查找完成之前,浏览器无法从主机名下载任何东西。
缓存DNS查找以获得更好的性能,这种缓存可以在由用户的ISP或局域网维护的特殊缓存服务器上进行,但也可以在用户的计算机上进行缓存。DNS信息被保存在操作系统的DNS缓存中(微软Windows上的”DNS客户端服务”)。大多数浏览器都有自己的缓存,独立于操作系统的缓存。只要浏览器将DNS记录保存在自己的缓存中,它就不会打扰操作系统去查询DNS。
IE默认缓存DNS查找30分钟,写在DnsCacheTimeout注册表设置中。Firefox缓存1分钟,可以用network.dnsCacheExpiration配置项设置。(Fasterfox把缓存时间改成了1小时 P.S. Fasterfox是FF的一个提速插件)
如果客户端的DNS缓存为空时(包括浏览器的和操作系统的),DNS查询的次数等同于网页中各域名的个数。包括该网页URL、图片、脚本文件、样式表、FLASH对象等使用的域名。减少域名数量可以减少DNS查询次数。
减少域名主机数量同时也减少了页面中发生的并行下载量,避免DNS查找可以减少响应时间,但是减少并行下载数量可能会增加响应时间。我的原则是把组件分散在2到4个主机名下,这是同时减少DNS查找和允许高并发下载的折中方案。
10.压缩JavaScript和CSS
分类: javascript, css
压缩具体来说就是从代码中去除不必要的字符以减少大小,从而提升加载速度。代码最小化就是去掉所有注释和不必要的空白字符(空格,换行和tab)。在JavaScript中这样做能够提高响应性能,因为要下载的文件变小了。两个最常用的JavaScript代码压缩工具是JSMin和YUI Compressor,YUI compressor还可以压缩CSS。
混淆是一种可选的源码优化措施,要比压缩更复杂,所以混淆过程也更容易产生bug。在对美国前十的网站调查中,压缩可以缩小21%,而混淆能缩小25%。虽然混淆的缩小程度更高,但比压缩风险更大。
除了压缩外部脚本和样式,行内的<script>和<style>块也可以压缩,即使启用了gzip模块,缩小它们仍然会减小5%或更多的大小。随着JavaScript和CSS的使用和大小的增加,压缩代码会有不错的效果。
11.避免重定向
分类: 内容
重定向使用301和302状态码完成。以下是301响应中的HTTP头信息示例:
HTTP/1.1 301 Moved Permanently
Location: http://example.com/newuri
Content-Type: text/html
浏览器会自动跳转到Location域指明的URL。重定向需要的所有信息都在HTTP头部,而响应体一般是空的。其他的HTTP头,比如Expires和Cache-Control也表示重定向。除此之外还有别的跳转方式:refresh元标签和JavaScript,但如果你必须得做重定向,最好用标准的3xxHTTP状态码,主要是为了让返回按钮能正常使用。
牢记重定向会拖慢用户体验,在用户和HTML文档之间插入重定向会延迟页面上的所有东西,页面无法渲染,组件也无法开始下载,直到HTML文档被送达浏览器。
有一种常见的极其浪费资源的重定向,而且web开发人员一般都意识不到这一点,就是URL尾部缺少一个斜线的时候。例如,跳转到http://astrology.yahoo.com/astrology会返回一个重定向到http://astrology.yahoo.com/astrology/的301响应(注意添在尾部的斜线)。在Apache中可以用Alias
,mod_rewrite
或者DirectorySlash
指令来取消不必要的重定向。
重定向最常见的用途是把旧站点连接到新的站点,还可以连接同一站点的不同部分,针对用户的不同情况(浏览器类型,用户帐号类型等等)做一些处理。用重定向来连接两个网站是最简单的,只需要少量的额外代码。虽然在这些时候使用重定向减少了开发人员的开发复杂度,但降低了用户体验。一种替代方案是用Alias和mod_rewrite,前提是两个代码路径都在相同的服务器上。如果是因为域名变化而使用了重定向,就可以创建一条CNAME(创建一个指向另一个域名的DNS记录作为别名)结合Alias或者mod_rewrite指令。
12.去除重复脚本
分类: javascript
页面含有重复的脚本文件会影响性能,这可能和你想象的不一样。在对美国前10大web站点的评审中,发现只有2个站点含有重复脚本。两个主要原因增加了在单一页面中出现重复脚本的几率:团队大小和脚本数量。在这种情况下,重复脚本会创建不必要的HTTP请求,执行无用的JavaScript代码,而影响页面性能。
IE会产生不必要的HTTP请求,而Firefox不会。在IE中,如果一个不可缓存的外部脚本被页面引入了两次,它会在页面加载时产生两个HTTP请求。即使脚本是可缓存的,在用户重新加载页面时也会产生额外的HTTP请求。
除了产生没有意义的HTTP请求之外,多次对脚本求值也会浪费时间。因为无论脚本是否可缓存,在Firefox和IE中都会执行冗余的JavaScript代码。
避免不小心把相同脚本引入两次的一种方法就是在模版系统中实现脚本管理模块。典型的脚本引入方法就是在HTML页面中用SCRIPT标签:
<script type="text/javascript" src="menu_1.0.17.js"></script>
PHP中一个可选方案是创建一个叫insertScript的函数:
<?php insertScript("menu.js") ?>
除了防止相同脚本被多次引入,这个函数还可以解决脚本相关的其它问题,比如依赖性检查和给脚本文件名添加版本号来支持“永久”有效期HTTP头。
13.配置ETags
分类: 服务器
实体标签(ETags),是服务器和浏览器用来决定浏览器缓存中组件与源服务器中的组件是否匹配的一种机制(“实体”也就是组件:图片,脚本,样式表等等)。添加ETags可以提供一种实体验证机制,比最后修改日期更加灵活。一个ETag是一个字符串,作为一个组件某一具体版本的唯一标识符。唯一的格式约束是字符串必须用引号括起来,源服务器用相应头中的ETag
来指定组件的ETag:
HTTP/1.1 200 OK
Last-Modified: Tue, 12 Dec 2006 03:03:59 GMT
ETag: "10c24bc-4ab-457e1c1f"
Content-Length: 12195</pre>
然后,如果浏览器必须验证一个组件,它用If-None-Match
请求头来把ETag传回源服务器。如果ETags匹配成功,会返回一个304状态码,这样就减少了12195个字节的响应体。
GET /i/yahoo.gif HTTP/1.1
Host: us.yimg.com
If-Modified-Since: Tue, 12 Dec 2006 03:03:59 GMT
If-None-Match: "10c24bc-4ab-457e1c1f"
HTTP/1.1 304 Not Modified</pre>
ETags存在的问题是它们是由特定服务器构造的,所以如果浏览器从一个服务器获取最初的组件,然后想验证另一个服务器上的相同组件,ETags是无法匹配成功的,而用一群服务器处理请求在web站点中又非常普遍。默认情况下,Apache和IIS会在ETag中嵌入数据,以大大降低在多服务器站点上有效性测试成功的几率。
Apache 1.3和2.x中ETag的格式是inode-size-timestamp
。就算给定的文件可能在多个服务器的相同目录下,而且文件大小、访问权限、时间戳等等全部相同,它的i节点(P.S. inode,UNIX中的索引文件)在不同服务器中也不一样。
IIS5.0和6.0也都存在类似的问题。IIS中ETags的格式是Filetimestamp:ChangeNumber
,ChangeNumber
是一个用来追踪IIS配置变更的计数器。 一个站点在不同的IIS服务器上的ChangeNumber
是不可能相同的。
最终结果是Apache和IIS为完全相同的组件生成的ETags无法跨浏览器匹配,如果ETags不匹配,用户就无法收到为又小又快的304响应设计的ETags。反而,他们将收到一个携带着组件所有数据的200正常响应。如果站点部署在单一服务器上,就根本不存在这个问题。但如果站点部署在多个服务器上,而且打算用Apache或者IIS的默认ETags配置,用户将看到缓慢的页面,服务器负载更高,还会消耗更大的带宽,并且代理也无法有效缓存页面内容。即使组件有“永久”Expires
HTTP头,用户点击重新加载或者刷新的时候,仍然会发出条件GET请求。
如果不想用ETags提供的灵活的验证模型,最好把所有的Etag全都去掉,可以用基于组件的时间戳的Last-Modified
HTTP头验证,而且去掉ETag可以减少HTTP响应头以及后续请求的大小。Microsoft Support article里写了怎样移除ETags。在Apache中,可以简单地通过在Apache配置文件中添上如下代码来实现:
FileETag none
14.让Ajax可缓存
分类: 内容
Ajax的一个好处是可以给用户提供即时反馈,因为它能够从后台服务器异步请求信息。然而,用了Ajax也无法保证用户在等待异步JavaScript和XML响应返回期间不会无聊。在很多应用程序中,用户是否保持等待取决于如何使用Ajax。例如,在基于web的电子邮件客户端中,用户为了寻找符合他们搜索标准的邮件消息,将会保持对Ajax请求返回结果的关注。重要的是,要记得“异步”并不意味着“即时”。
要提高性能,优化这些Ajax响应至关重要。最重要的提高Ajax性能的方法就是让响应变得可缓存,就像在添上Expires或者Cache-Control HTTP头中讨论的一样。下面适用于Ajax的其它规则:
我们一起看看例子,一个Web 2.0的电子邮件客户端用了Ajax来下载用户的通讯录,以便实现自动完成功能。如果用户从上一次使用之后再没有修改过她的通讯录,而且Ajax响应是可缓存的,有尚未过期的Expires或者Cache-Control HTTP头,那么之前的通讯录就可以从缓存中读出。必须通知浏览器,应该继续使用之前缓存的通讯录响应,还是去请求一个新的。可以通过给通讯录的Ajax URL里添加一个表明用户通讯录最后修改时间的时间戳来实现,例如&t=1190241612。如果通讯录从上一次下载之后再没有被修改过,时间戳不变,通讯录就将从浏览器缓存中直接读出,从而避免一次额外的HTTP往返消耗。如果用户已经修改了通讯录,时间戳也可以确保新的URL不会匹配缓存的响应,浏览器将请求新的通讯录条目。
即使Ajax响应是动态创建的,而且可能只适用于单用户,它们也可以被缓存,而这样会让你的Web 2.0应用更快。
15.尽早清空缓冲区
分类: 服务器
当用户请求一个页面时,服务器需要用大约200到500毫秒来组装HTML页面,在这期间,浏览器闲等着数据到达。PHP中有一个flush()函数,允许给浏览器发送一部分已经准备完毕的HTML响应,以便浏览器可以在后台准备剩余部分的同时开始获取组件,好处主要体现在很忙的后台或者很“轻”的前端页面上(P.S. 也就是说,响应时耗主要在后台方面时最能体现优势)。
比较理想的清空缓冲区的位置是HEAD后面,因为HTML的HEAD部分通常更容易生成,并且允许引入任何CSS和JavaScript文件,这样就可以让浏览器在后台还在处理的时候就开始并行获取组件。
例如:
...
</head>
<?php flush(); ?>
<body>
...
Yahoo!搜索开创了这项技术,而且真实用户测试研究也证明了使用这种技术的诸多好处。
16.对Ajax用GET请求
Yahoo!邮箱团队发现使用XMLHttpRequest
时,浏览器的POST请求是通过一个两步的过程来实现的:先发送HTTP头,在发送数据。所以最好用GET请求,它只需要发送一个TCP报文(除非cookie特别多)。IE的URL长度最大值是2K,所以如果要发送的数据超过2K就无法使用GET了。
一个有趣的副作用是没有实际发布任何数据的POST就像GET一样。 根据HTTP规范,GET用于检索信息,因此当您仅请求数据时,使用GET是有意义的(语义上),而不是将数据发送到服务器端存储。
17.延迟加载组件
分类: 内容
你可以仔细查看页面并问自己:什么才是一开始渲染页面所必须的?其余内容都可以等会儿。
JavaScript是分隔onload事件之前和之后的一个理想选择。例如,如果有JavaScript代码和支持拖放以及动画的库,这些都可以先等会儿,因为拖放元素是在页面最初渲染之后的。其它可以延迟加载的部分包括隐藏内容(在某个交互动作之后才出现的内容)和折叠的图片。
工具可帮你减轻工作量:YUI Image Loader可以延迟加载折叠的图片,还有YUI Get utility是一种引入JS和CSS的简单方法。Yahoo!主页就是一个例子,可以打开Firebug的网络面板仔细看看。
最好让性能目标符合其它web开发最佳实践,比如“渐进增强”。如果客户端支持JavaScript,可以提高用户体验,但必须确保页面在不支持JavaScript时也能正常工作。所以,在确定页面运行正常之后,可以用一些延迟加载脚本增强它,以支持一些拖放和动画之类的华丽效果。
18.预加载组件
分类: 内容
预加载可能看起来和延迟加载是相反的,但它其实有不同的目标。通过预加载组件可以充分利用浏览器空闲的时间来请求将来会用到的组件(图片,样式和脚本)。用户访问下一页的时候,大部分组件都已经在缓存里了,所以在用户看来页面会加载得更快。
实际应用中有以下几种预加载的类型:
- 无条件预加载——尽快开始加载,获取一些额外的组件。google.com就是一个sprite图片预加载的好例子,这个sprite图片并不是google.com主页需要的,而是搜索结果页面上的内容。
- 条件性预加载——根据用户操作猜测用户将要跳转到哪里并据此预加载。在search.yahoo.com的输入框里键入内容后,可以看到那些额外组件是怎样请求加载的。
- 提前预加载——在推出新设计之前预加载。经常在重新设计之后会听到:“这个新网站不错,但比以前更慢了”,一部分原因是用户访问先前的页面都是有旧缓存的,但新的却是一种空缓存状态下的体验。可以通过在将要推出新设计之前预加载一些组件来减轻这种负面影响,老站可以利用浏览器空闲的时间来请求那些新站需要的图片和脚本。
19.减少DOM元素的数量
分类: 内容
一个复杂的页面意味着要下载更多的字节,而且用JavaScript访问DOM也会更慢。举个例子,想要添加一个事件处理器的时候,循环遍历页面上的500个DOM元素和5000个DOM元素是有区别的。
大量的DOM元素是一种征兆——页面上有些内容无关的标记需要清理。您是否使用嵌套表进行布局? 你是否只是为了解决布局问题而投入更多的<div>? 也许有一种更好,更语义正确的标记方式。
YUI CSS utilities对布局有很大帮助:grids.css针对整体布局,fonts.css和reset.css可以用来去除浏览器的默认格式。这是个开始清理和思考标记的好机会,例如只在语义上有意义的时候使用<div>
,而不是因为它能够渲染一个新行。
DOM元素的数量很容易测试,只需要在Firebug的控制台里输入:
document.getElementsByTagName('*').length
那么多少DOM元素才算是太多呢?可以参考其它类似的标记良好的页面,例如Yahoo!主页是一个相当繁忙的页面,但只有不到700个元素(HTML标签)。
20.跨域分离组件
分类: 内容
分离组件可以最大化并行下载,但要确保只用不超过2-4个域,因为存在DNS查找的代价。例如,可以把HTML和动态内容部署在www.example.org,而把静态组件分离到static1.example.org和static2.example.org。更多信息请查看Tenni Theurer和Patty Chi的文章:Maximizing Parallel Downloads in the Carpool Lane
21.尽量少用iframe
分类: 内容
用iframe可以把一个HTML文档插入到父文档里,重要的是明白iframe是如何工作的并高效地使用它。
<iframe>的优点:
- 引入缓慢的第三方内容,比如标志和广告
- 安全沙箱
- 并行下载脚本
<iframe>的缺点:
- 代价高昂,即使是空白的iframe
- 阻塞页面加载
- 非语义
22.杜绝404
分类: 内容
HTTP请求代价高昂,完全没有必要用一个HTTP请求去获取一个无用的响应(比如404 Not Found),只会拖慢用户体验而没有任何好处。
有些站点用的是有帮助的404——“你的意思是xxx?”,这样做有利于用户体验,,但也浪费了服务器资源(比如数据库等等)。最糟糕的是链接到的外部JavaScript有错误而且结果是404。首先,这种下载将阻塞并行下载。其次,浏览器会试图解析404响应体,因为它是JavaScript代码,需要找出其中可用的部分。
23.给Cookie减肥
分类: cookie
使用cookie的原因有很多,比如授权和个性化。HTTP头中cookie信息在web服务器和浏览器之间交换。重要的是保证cookie尽可能的小,以最小化对用户响应时间的影响。
更多信息请查看Tenni Theurer和Patty Chi的文章:When the Cookie Crumbles。相关经验原则可以总结如下:
- 清除不必要的cookie
- 保证cookie尽可能小,以最小化对用户响应时间的影响
- 注意给cookie设置合适的域级别,以免影响其它子域
- 设置合适的有效期,更早的有效期或者none可以更快的删除cookie,提高用户响应时间
24把组件放在不含cookie的域下
分类: cookie
当浏览器发送对静态图像的请求时,cookie也会一起发送,而服务器根本不需要这些cookie。所以它们只会造成没有意义的网络通信量,应该确保对静态组件的请求不含cookie。可以创建一个子域,把所有的静态组件都部署在那儿。
如果域名是www.example.org,可以把静态组件部署到static.example.org。然而,如果已经在顶级域example.org或者www.example.org设置了cookie,那么所有对static.example.org的请求都会含有这些cookie。这时候可以再买一个新域名,把所有的静态组件部署上去,并保持这个新域名不含cookie。Yahoo!用的是yimg.com,YouTube是ytimg.com,Amazon是images-amazon.com等等。
把静态组件部署在不含cookie的域下还有一个好处是有些代理可能会拒绝缓存带cookie的组件。有一点需要注意:如果不知道应该用example.org还是www.example.org作为主页,可以考虑一下cookie的影响。省略www的话,就只能把cookie写到*.example.org,所以因为性能原因最好用www子域,并且把cookie写到这个子域下。
25.尽量减少DOM访问
分类: javascript
用JavaScript访问DOM元素是很慢的,所以,为了让页面反应更迅速,应该:
- 缓存已访问过的元素的索引
- 先“离线”更新节点,再把它们添到DOM树上
- 避免用JavaScript修复布局问题
更多信息请查看YUI影院里Julien Lecomte的文章:High Performance Ajax Applications
26.用智能的事件处理器
分类: javascript
有时候感觉页面反映不够灵敏,是因为有太多频繁执行的事件处理器被添加到了DOM树的不同元素上,这就是推荐使用事件委托的原因。如果一个div
里面有10个按钮,应该只给div容器添加一个事件处理器,而不是给每个按钮都添加一个。事件能够冒泡,所以可以捕获事件并得知哪个按钮是事件源。
不需要为了处理DOM树而等待onload事件,通常只要目标元素在DOM树中可访问即可,而不必等待所有的图片下载完成。可以考虑用DOMContentLoaded
来代替onload事件,但为了让它在所有浏览器中都可用,可以用YUI Event工具,它有一个[onAvailable](https://developer.yahoo.com/yui/event/#onavailable)
方法。
更多信息请查看YUI影院里Julien Lecomte的文章:High Performance Ajax Applications
27.选择<link>舍弃@import
分类: css
前面提到了一个最佳实践:为了实现逐步渲染,CSS应该放在顶部。
在IE中用@import与在底部用<link>效果一样,所以最好不要用它。
28.避免使用滤镜
分类: css
IE专有的AlphaImageLoader滤镜可以用来修复IE7之前的版本中半透明PNG图片的问题。在图片加载过程中,这个滤镜会阻塞渲染,卡住浏览器,还会增加内存消耗而且是被应用到每个元素的,而不是每个图片,所以会存在一大堆问题。
最好的方法是干脆不要用AlphaImageLoader,而优雅地降级到用在IE中支持性很好的PNG8图片来代替。如果非要用AlphaImageLoader,应该用下划线hack:_filter来避免影响IE7及更高版本的用户。
29.优化图片
分类: 图片
设计师做好图片后,在把这些图片通过FTP上传到web服务器之前,我们还可以做一些事情。
- 检查GIF图片,看看图片中是不是用了调色板大小对应的颜色数,用imagemagick可以简单地检查:
identify -verbose image.gif
如果4色图片用了调色板中256色的“槽”,那就还有改进的余地 - 试着把GIF图片转换成PNG,看能不能缩减大小,往往可以。开发者通常不愿意用PNG图片,因为浏览器支持有限,但这都是过去的事情了。真正的问题是PNG图片完全支持alpha透明度,而GIF图片却不支持透明度渐变,所以GIF能做的任何事情,PNG都可以(除了动画)。下面这个简单的命令就能让PNG图片可以安全使用:
convert image.gif image.png
“我们强调的是:给PNG一个机会。” - 用pngcrush(或者其它的PNG优化工具)处理所有的PNG图片,例如:
pngcrush image.png -rem alla -reduce -brute result.png
- 用jpegtran处理所有JPEG图片,这个工具支持对JPEG图片的无损操作比如旋转,还可以用来去除注释和其它无用信息(比如EXIF信息 P.S. 数码照片信息,焦距光圈之类的):
jpegtran -copy none -optimize -perfect src.jpg dest.jpg
30.优化CSS Sprite
分类: 图片
- 在Sprite图片中横向排列一般都比纵向排列的最终文件小
- 组合Sprite图片中的相似颜色可以保持低色数,最理想的是256色以下PNG8格式
- “对移动端友好”,不要在Sprite图片中留下太大的空隙。虽然不会在很大程度上影响图片文件的大小,但这样做可以节省用户代理把图片解压成像素映射时消耗的内存。100×100的图片是1万个像素,而1000×1000的图片就是100万个像素了。
31.不要用HTML缩放图片
分类: 图片
不要因为在HTML中可以设置宽高而使用本不需要的大图。如果需要
<img width="100" height="100" src="mycat.jpg" alt="My Cat" />
那么图片本身(mycat.jpg)应该是100x100px的,而不是去缩小500x500px的图片。
32.用小的可缓存的favicon.ico(P.S. 收藏夹图标)
分类: 图片
favicon.ico是放在服务器根目录的图片,它会带来一堆麻烦,因为即便你不管它,浏览器也会自动请求它,所以最好不要给一个404 Not Found
响应。而且只要在同一个服务器上,每次请求它时都会发送cookie,此外这个图片还会干扰下载顺序,例如在IE中,当你在onload中请求额外组件时,将会先下载favicon。
所以为了缓解favicon.ico的缺点,应该确保:
- 足够小,最好在1K以下
- 设置合适的有效期HTTP头(以后如果想换的话就不能重命名了),把有效期设置为几个月后一般比较安全,可以通过检查当前favicon.ico的最后修改日期来确保变更能让浏览器知道。
Imagemagick可以用来处理小收藏夹图标
33.保证所有组件都小于25K
分类: 移动
这个限制是因为iPhone不能缓存大于25K的组件,注意这里指的是未压缩的大小。这就是为什么缩减内容本身也很重要,因为单纯的gzip可能不够。
更多信息请查看Wayne Shea和Tenni Theurer的文章:Performance Research, Part 5: iPhone Cacheability – Making it Stick
34.把组件打包到一个复合文档里
分类: 移动
把各个组件打包成一个像有附件的电子邮件一样的复合文档里,可以用一个HTTP请求获取多个组件(记住一点:HTTP请求是代价高昂的)。用这种方式的时候,要先检查用户代理是否支持(iPhone就不支持)。
35.避免图片src属性为空
分类: 服务器
Image with empty string src属性是空字符串的图片很常见,主要以两种形式出现:
-
straight HTML
<img src=””>
-
JavaScript
var img = new Image();
img.src = “”;
这两种形式都会引起相同的问题:浏览器会向服务器发送另一个请求。
- IE向页面所在目录发起一个请求
- Safari和Chrome想当前页面本身发送一个请求
- Firefox3及更早版本与Safari和Chrome处理方式一样,但3.5解决了这个问题[bug 444931],不会再发送请求了
- Opera遇到有空src属性的图片不做任何处理
为什么图片src属性为空不好?
- 意外发送大量的通信量对服务器来说是很伤的,尤其是在每天有几百万访问量页面的时候。
- 浪费服务器资源去生成一个根本不可能被看到的页面
- 可能会污染用户数据,如果追踪请求状态,要么通过cookie要么是其它方式,可能会破坏用户数据。即使图片请求并没有返回图片,整个HTTP头部也会被浏览器接受并读取,包括所有的cookie。虽然其余部分会被丢弃,但这可能已经造成破坏了。
问题的根本原因是各个浏览器在处理URI时的分歧,这在RFC 3986 – Uniform Resource Identifiers文档中有明确定义。如果URI是一个空串,会被看作一个相对URI,并按照5.2节定义的算法处理。实际情况是,Firefox、Safari和Chrome都是根据文档中5.4节列出的规范来处理空串,而IE并没有正确处理。据说在旧版本规范文档RFC 2396 – Uniform Resource Identifiers(被RFC 3986废弃了)中,所以从严格意义上来说浏览器处理相对URI的做法都是对的。问题是,在这种情形下,空串显然是无心的(P.S. 而不是什么相对URI)。
HTML5的4.8.2节有关于<img>标签src属性的描述,规定浏览器不再发送额外请求:
The src attribute must be present, and must contain a valid URL referencing a non-interactive, optionally animated, image resource that is neither paged nor scripted. If the base URI of the element is the same as the document’s address, then the src attribute’s value must not be the empty string.(P.S. “src属性必须存在,而且必须有一个合法的URL引用非交互式的动画或者图像资源,不能分页也不能含有脚本。如果元素的基URI和文档地址相同,那么src属性的值就不能是空串。”)
希望将来浏览器不会存在这个问题,不幸的是,没有针对<script src=””>和<link href=””>的条款,也许还有时间调整,以确保浏览器不会意外地实现这一行为。
这条原则是受了Yahoo!的JavaScript大师Nicolas C. Zakas的启发,更多信息请查看他的文章:Empty image src can destroy your site
再贴几个地址文章很好
前端工程与性能优化