提高 Web 站点性能的最佳实践
原文地址:http://developer.yahoo.com/performance/rules.html
本文在尊重原文基础上,尽量翻译得通俗易懂一些。
本文内容
- 提高 Web 站点性能的最佳实践
- 最大限度减少 HTTP 请求
- 使用内容分发网络(CDN)
- 添加 Expires 或 Cache – Control 头
- Gzip 组件
- CSS 放在页面顶部
- JavaScript 放在页面底部
- 避免 CSS 表达式
- 使用外部 JavaScript 和 CSS
- 减少 DNS 查询
- 精简 JavaScript 和 CSS
- 避免重定向
- 删除重复的脚本
- 配置 ETags
- 使得 Ajax 可缓存
- 尽早强制地发送缓冲给客户端
- 用 GET 发送 Ajax 请求
- 延迟加载组件
- 预加载组件
- 减少 DOM 元素数量
- 根据域名划分页面内容
- 最小化 iframe 数量
- 不要出现 404 错误
- 减小 Cookie 的大小
- 对组件使用无 coockie 域名
- 最小化 DOM 访问
- 开发智能的事件处理程序
- 用 <link> 代替 @import
- 避免使用滤镜
- 优化图像
- 优化 CSS Sprites
- 不要在 HTML 中缩放图像
- favicon.ico 要小且可缓存
- 保持组件 25K 以下
- 把组件打包到一个 Multipart 文档
- 避免图片 src 属性为空
- 修改记录
下面是使页面更快的35个最佳实践,它们被划分为7个类别。
类别: content、server、cookie、css、javascript、images、mobile
(Yahoo 开发的浏览器插件 YSLOW,利用这七个类作为评价页面的指标)
最大限度减少 HTTP 请求
类别: content
最终用户(the end-user)80%的响应时间花费在前端(the front-end)。大部分时间用来下载页面中的所有组件:图像、CSS、JS、Flash 等。因此,反过来,减少页面组件的数量,就可以减少渲染(呈现)页面所需的 HTTP 请求的数量。这是加快页面的关键。
一个方法当然是简化页面设计,减少页面组件的数量。但是否有方法,既构建具有丰富内容的网页,也实现了快速响应?下面是减少 HTTP 请求数量的技巧,也提供了丰富的网页设计。
- 合并文件。通过把所有脚本或 CSS 合并到一个单独的文件,来减少 HTTP 请求的数量。当在不同的页面中,脚本和 CSS 都不太一样时,合并就比较困难。你可以把合并放到最后部署时,从而改进响应时间。
- CSS 精灵(CSS Sprites)。CSS Sprites是减少图像请求数量的首选方法。把你的背景图像合并到一个单独的图像,并且使用CSS 的“background-image”和“background-position”属性,来显示你所需的图像部分。
CSS Sprites 是一种 CSS 图像拼合技术,一种网页图片应用处理方式。
- 图像地图(Image maps)。把多个图像合并到一个单独的图像。合并前与合并后的图像总体大小相同,而且减少了HTTP请求的数量,加快了页面速度。但只有当图像在页面中是连续的,Image maps才好用,如导航栏。定义图像地图的坐标很枯燥,而且容易出错。对导航使用图像地图不具有可访问性,所以不推荐。
- 内嵌图片(Inline images)。在页面中,使用 data: URL scheme 嵌入图像数据。这会增加页面大小。Inline images 与 CSS(已缓存)相结合可以减少 HTTP 请求,避免增加页面大小。目前,所有主流浏览器尚不支持 Inline images。
减少页面的HTTP请求的数量首选要做的事,对于改善用户初次访问页面的性能,这是最重要准则。正如 Tenni Theurer 在其文章 Browser Cache Usage - Exposed! 指出的,每天访问你网站的 40-60% 人都是无缓存的(都是初次访问,无本地缓存)。使页面对初次访问更快,是更好的用户体验的关键。这些首次访问者的页面快速更好的用户体验是关键。
批注
一个页面往往包含很多资源,比如图像、JS、CSS 等,而这些资源都在服务器上,客户端若想显示这个页面,必须通过网络从服务器下载资源到本地。可想而知,虽然每个资源都很小,也就在几十 K 左右,但是数量很多,客户端每次需要这样资源,都会到服务器上下载,因此,减少下载的次数,或是下载完后,直接从浏览器缓存里获得,将很有意义。
本节的技巧:合并文件、CSS Sprites、Image maps、Inline images 都是为了减少下载次数。比如,如果一个页面需要三个 CSS,那么在用户初次访问页面时,需要向服务器请求三次。因此,若将这三个 CSS 合并成一个,那么只需下载一次;图像也是如此,一个页面最多的就是图像,试想网站的导航条,与其为每个操作都搞一个图片,倒不如将这些图片合并在一个图像上,再通过 CSS 获得图片局部区域。
使用内容分发网络(CDN)
类别: server
用户“接近”你Web服务器的程度会影响响应时间。把内容部署在多个、地理位置分散的服务器上,会使页面加载的速度从用户角度看更快。但是我们应该从哪里开始?
作为实现地理位置分散内容的第一步,不要试图重新设计你的Web应用程序,使它运行在一个分布式的结构中。根据应用程序,改变结构,包括跨服务器同步会话状态和复制数据库事务等,这些艰巨的任务。根据不同的应用,改变结构可以包括跨服务器的位置同步会话状态和复制数据库交易等艰巨任务。尝试减少用户和内容之间的距离,可以延迟,或从不通过,这是应用程序结构的步骤。
记住,最终用户的80-90% 响应时间花费在下载所有页面的组件:图像、CSS、JS、Flash 等,这是提高性能的黄金法则。最好先分散你的静态内容,如图像、CSS、JS、Flash 等,而不是重新设计应用程序结构艰巨的工作开始。由于内容发布网络,不仅大幅度减少了响应时间,而且简化了应用程序。
一个 CDN 是一个处于多个位置的 Web 服务器的集合,更有效地向用户发送内容。选择哪个服务器发送内容给特定用户通常是基于一个网络评估。例如,选择最少的网络跳数或最快的响应时间。
一些大型的互联网公司拥有自己的 CDN,而通过 CDN 服务提供商,如 Akamai Technologies, EdgeCast 或 level3,成本则很高。对于刚成立的公司和私人站点,一个 CDN 服务的成本可以让人望而却步,但当你越来越受关注,并全球化时,一个 CDN 是必需的,以便快速响应。以 Yahoo! 为例,他们把静态内容从应用程序中移到 CDN(上面提到 CDN 服务提供商,以及他们自己的 CDN)上,提高了最终用户 20% 以上的响应时间。使用 CDN 是一个只需要相对简单地修改代码,显著改善站点速度的方法。
批注
客户端访问服务器,要经过路由,是要计算代价的,最小跳数也好,最少响应时间也罢,所以将自己的网站内容部署在多个地理位置是有必要的。
添加 Expires 或 Cache – Control 头
类别: server
此规则有两个方面:
- 对于静态组件:设置 Expires 头为 "Never expire" 策略——“永不过期”;
- 对于动态组件:使用适当的 Cache – Control 头,帮助浏览器有条件地发送请求。
随着页面越来越丰富,这意味着更多的 JS、CSS、图像和 Flash。一个初次访问页面的用户会发出很多 HTTP 请求,但是通过 Expires 头,你可以使那些组件被浏览器缓存。在之后的页面浏览,就避免了不必要的 HTTP 请求。Expires 头经常用在图像,但也可以用在包括 JS、CSS 和 Flash 所有组件。
浏览器(和代理)使用缓存来减少 HTTP 请求的次数和规模,使页面加载速度更快。一个 Web 服务器在 HTTP 响应中使用 Expires 头,会告诉客户端这个组件被缓存多长时间。下面 Expires 头告诉浏览器,这个响应直到 2011年4月15日 都是可靠的。
Expires: Thu, 15 Apr 2011 20:00:00 GMT
如果你的服务器是 Apache,使用 ExpiresDefault 指令来设置相对于当前日期的过期时间。下面例子 ExpiresDefault 指令设置过期时间为发出请求后的 10年。
ExpiresDefault "access plus 10 years"
记住,如果使用 Expires 头,那么当组件改变时,你必须改变组件的文件名。以 Yahoo 为例,常常使这步作为生成过程的一部分:一个版本号内置在组件文件名,如 yahoo_2.0.6.js。
使用 Expires 头只影响那些用户已经浏览过的页面。当用户初次访问,浏览器缓存为空时,不会影响 HTTP 请求的数量。因此,这种性能改善的影响取决于用户多长时间会在 primed cache (primed cache 是已经包含页面中的所有组件,它与 Empty Cache 相对)命中你的页面。我们在 Yahoo 做了测试,发现在 primed cache 浏览页面的频率是 75-85 %。通过使用 Expires 头,增加被浏览器缓存的组件数量,在接下来的浏览中可以重用,而无需通过用户的网络连接发送任何字节。
批注
根据 Yahoo 的统计,用户从缓存中获得页面所有组件的频率在 75-85 %,那么我们很有必要告诉浏览器如何缓存页面资源。比如图像、CSS 这样的静态资源,就告诉浏览器永不过期。而对动态资源,则告诉浏览器,要有条件的请求,别重新请求。
Gzip 组件
类别: server
通过网络传输 HTTP 请求和响应所花费的时间,可以通过前端机制而得到显著减少。事实上,最终用户的宽带速度、Internet 服务器提供商、点对点交换接近程度等等,因素不是开发团队能控制的,但是,还有其他影响响应时间的因素(这些是可以控制的)。压缩,通过减小 HTTP 响应的大小,来减少响应时间。
从 HTTP/1.1 开始,Web 客户端用 HTTP 请求中的 Accept – Encoding 头来指示是否支持压缩。
Accept-Encoding: gzip, deflate
如果 Web 服务器在请求中看到这个头,它可以利用客户端列出的方法之一压缩响应。Web服 务器通过响应的 Content – Encoding 头通知客户端。
Content-Encoding: gzip
Gzip 是目前最流行和最有效的压缩方法。它是由 GNU 开发的项目,并通过 RFC 1952 标准化。你可能看过其他压缩格式——deflate,但它的效率较差,不太流行。
Gzip 压缩一般可以减少约 70% 的响应大小。目前大约有 90% 通过浏览器的互联网流量,都声称支持Gzip,今天的互联网流量约 90% 穿过声称支持 gzip 的浏览器。如果你使用 Apache,配置 Gzip 模块取决于你的版本:Apache 1.3 使用 mod_gzip ,而 Apache 2.x mod_deflate。
总所周知,浏览器和代理带来的问题是,可能会导致浏览器期望的与它收到的压缩内容不匹配。幸好,这种特殊情况随着旧式浏览器使用的减少在减少。Apache 模块会自动添加变化的响应头来解决这个问题。
服务器选择什么压缩成 gzip,要根据文件类型,但通常很有限。大多数网站压缩他们的 HTML 文件,脚本和 CSS 也很值得压缩,但是很多站点错过了这个机会。事实上,压缩任何文本响应,包括 XML 和 JSON,都是值得的。图像和 PDF 文件不应该被压缩,因为它们已经被压缩了。试图压缩他们,不仅浪费 CPU,还会潜在增加文件的大小。
用 Gzip 压缩尽可能多的文件类型是一种减小页面大小,加速用户体验的简单方法。
批注
如果你了解 HTTP 协议,或是用相关网页测试工具,那么一定知道服务器对客户端都响应了些什么。浏览器接收这些内容后,解析,并呈现给用户。无论是接收 CSS 、脚本文件,还是图像,那么服务器的响应的时候,将它们压缩,再发送给客户端,就是很符合逻辑的结果。
CSS 放在页面顶部
类别:css
在研究 Yahoo! 的性能时,我们发现,把 CSS 放到 HEAD 标记使得页面加载快了。这是因为,把 CSS 放在 HEAD 标记使得页面逐渐地呈现。
关心性能的前端工程师期望一个页面能逐渐加载。也就是说,我们希望浏览器尽快显示内容。这对于拥有较多内容的页面和网速较慢的用户来说特别重要。给用户返回可见的反馈的重要性,比如进度指示,已经做了很好的研究,并形成了正式文档。在我们看来,HTML 页面就是进度指示器。当浏览器逐渐加载页面,头部、导航条、顶部 logo 等等,对于等待页面加载的用户来说,都可以作为可见的反馈信息。这便从整体上改善了用户体验。
把CSS放在接近文档底部的问题是,阻止在很多浏览器上逐渐呈现,包括 Internet Explorer。这些浏览器阻塞呈现是为了避免,如果式样改变,那么必须重绘页面元素。用户不得不面对一个空白页面。
HTML 规范清楚地指出 CSS 要包含在页面的 HEAD 区域:“与 A 不同,<link />只能出现在文档的 HEAD 区域,尽管它可以出现很多次。”无论是白屏,还是出现没有式样的内容,都是不值得的。最好的解决方法就是按照 HTML 规范,在文档的 HEAD 里加载 CSS。
批注
有种情况,你一定见过:当很多人下载电影,占了带宽,网页要么打不开,要么打开了,有内容没式样。浏览器解析服务器发过来的页面,总是有个顺序问题的。比如,在没获得 CSS 文件前,就准备呈现页面是没有意义的。
脚本放在页面底部
类别:javascript
脚本带来的问题是它阻止了并行下载。HTTP/1.1 规范建议,浏览器并行下载,每个主机不能超过 2 个组件。如果你的图片放在多个主机上,那么你可以从每个主机并行下载两个资源。然而,当下载脚本时,浏览器就不会下载其他资源,即便资源位于不同的主机。
在某些情况下,把脚本放在底部不太容易。比如,如果脚本使用 document.write 向页面插入内容,它就不能被往下移。这里还会有作用域问题。很多情况下,都会遇到这方面问题。
一个经常用的方法是使用延迟脚本(deferred scripts)。DEFER 属性指示,脚本不包含 document.write,告诉浏览器可以继续呈现。不幸的是,Firefox 并不支持 DEFER 属性。在 Internet Explorer 中,脚本可以被延迟,但效果可能不像期望的那样。如果脚本可以被延迟,那么它就可以被移到页面的底部,这将使页面加载加快。
批注
这点很容易理解。如果不考虑上面提到 document.write 情况,那么绝大多数脚本,要么是创建页面标记(控件),要么修改页面标记,无论从那个角度讲,脚本最后执行都是最合适的。
比如,你在 HEAD 里的脚本使用了页面的元素,那估计脚本会报错,因为元素那时还没有被创建。所以,CSS、JS 等页面资源,别乱放。
现在的 Ajax 框架,比如 jQuery 的 ready 方法,Ext.Net 的 Ext.onReady 方法等都是处于这个目的。
避免 CSS 表达式
类别:css
CSS 表达式是动态设置 CSS 属性强大(而危险)的方法。从 Internet Explorer 5 开始支持 CSS 表达式,但从 IE 8 开始被废弃。下面例子,使用 CSS 表达式实现每隔一个小时设置一次背景颜色:
background-color: expression( (new Date()).getHours()%2 ? "#B8D4FF" : "#F08A00" );
如上所示,CSS 表达式使用了一个 JavaScript 表达式。CSS 属性根据 JavaScript 表达式的计算结果来设置。表达式在其它浏览器中不起作用,因此,在跨浏览器的设计中,针对 Internet Explorer 设置属性会比较有用。
CSS 表达式的问题在于它的计算频率比我们想象得多。不仅在页面显示和缩放时,也在页面滚动,甚至在界面上移动鼠标,都会重新计算。给 CSS 表达式增加一个计数器可以让我们跟踪表达式何时计算以及计算频率。随便在页面里移动鼠标都可以轻松达到 10000 次以上。
减少 CSS 表达式计算次数的一个方法是使用一次性的表达式。当第一次计算表达式时,它将结果赋值给式样属性,并用这个值代替 CSS 表达式。如果样式属性必须在页面周期内动态地改变,那么一个可行的方法是使用事件处理,而不是 CSS 表达式。如果你必须使用 CSS 表达式,那么一定要记住,它们可能要运行成千上万次,有可能会影响以哦面性能。
批注
虽然有用,但存在的问题很突出、更要命。所以,还是不要使用的好。
使用外部 JavaScript 和 CSS
类别:javascript,css
很多性能规则都是如何管理外部文件?但在你思考这些问题前,你应该问一个更基本的问题:JavaScript 和 CSS 是应该放在外部文件中,还是应该内嵌在页面里?
实际中,使用外部文件通常可以提高页面速度,因为,JavaScript 和 CSS 文件可以被浏览器缓存。而内嵌在 HTML 文档中的 JavaScript 和 CSS 会在每次请求 HTML 文档时被重新下载。这虽然减少了所需的 HTTP 请求次数,却增加了 HTML 文档的大小。而另一方面,如果 JavaScript 和 CSS 在外部文件,并被浏览器缓存,那么在没有增加 HTTP 请求次数情况下,减少 HTML 文档的大小。
然而,关键问题是,被缓存的外部 JavaScript 和 CSS 组件的频率,与请求 HTML 文档的次数有关。尽管很难量化,但还是有很多指标来测量它。如果用户在每次会话中浏览你网站的多个页面,而这些页面重用了 脚本 和CSS,那么使用可以被浏览器缓存的外部文件将会带来很大好处。
很多的网站没能建立起这些指标。对这些网站,一般来说,最好的解决方法是把 JavaScript 和 CSS 作为外部文件来部署。适合采用内嵌代码的唯一例外是网站的主页,如 Yahoo!'s front page 和 My Yahoo!。主页在每次会话中很少被浏览,你会发现,在主页嵌入 JavaScript 和 CSS 对最终用户的响应时间更快。
对于拥有较大访问量的首页,有一种技术可以平衡嵌入代码带来的减少 HTTP 请求,与使用外部文件带来缓存的好处。其中一个技术就是在首页中嵌入 JavaScript 和 CSS,但完成加载后,动态下载外部文件。接下来的页面就会使用已经被浏览器缓存的外部文件。
批注
既然只有外部文件才可以被浏览器缓存,那么何乐不为呢。而且嵌入到页面脚本代码,也不好维护,同时,会增大页面大小。
减少 DNS 查询
类别:content
域名解析系统(DNS)提供域名和 IP 的映射,就像电话本映射人名与他们的电话号码一样。当你在浏览器地址栏键入 www.dudo.org 时,DNS 解析会返回给浏览器对应的 IP。DNS 解析是有时间代价的。一般情况下,查找给定域名对应的 IP,需要 20 到 120 毫秒。在这个过程中,浏览器不会下载任何东西,直到 DNS 查询完毕。
缓存 DNS 查询可以改善性能。DNS 缓存发生在要一个特定的缓存服务器,由用户的 ISP 或本地网络维护,但也会缓存在用户自己的机器上。DNS 信息保存在操作系统的 DNS 缓存中(微软 Windows 操作系统的 "DNS Client Service")。大多数浏览器都有自己的缓存,它独立于操作系统之外。只要浏览器在自己的缓存维护一个 DNS 信息,在一次请求中就不会受到操作系统的影响。
默认情况下,Internet Explorer 的 DNS 缓存为 30 分钟,由注册表的 DnsCacheTimeout 规定。Firefox 的 DNS 缓存为 1 分钟,由配置文件 network.dnsCacheExpiration 控制(Fasterfox 为 1 小时)。
当客户端的 DNS 缓存为空(浏览器和操作系统的 DNS 缓存都为空),DNS 查询的次数等于页面中主机的数量。这包括页面中的 URL、图像、JS、CSS、Flash 等使用的主机。减少唯一主机名的数量就可以减少 DNS 查询的次数。
但减少唯一主机名的数量潜在地减少了并行下载的数量。虽然避免 DNS 查询次数节省了响应时间,但是减少并行下载却增加了响应时间。我的原则是,把页面组件分割在 2 个到 4 个主机之间。这样就是在减少 DNS 查询次数与较高的并行下载之间获得了权衡。
批注
Yahoo 还真是,任何一个环节都不放过。但是试想一下,DNS 查询在 20-120 毫秒之间,这个时间里相当于可以从服务器上下载 1-2 个几十k的资源,也许是 CSS 文件,也许是脚本文件,也许是图片,所以减少 DNS 查询还是很有必要的。
精简 JavaScript 和 CSS
类别:javascript,css
“精简”是工程实践的结论,从代码中去掉不必要的字符,以减少文件大小,从而节省加载时间。去掉代码的所有注释、空白字符(包括空格、换行、tab)。在 JavaScript 中,这会提高响应时间,因为减少了下载文件的大小。精简 JavaScript 代码最流行、最广泛的两个工具是 JSMin 和 YUI Compressor。YUI Compressor 还可用于精简 CSS。
“混淆”是另外一种可用于源代码优化的方法。该方法要比精简复杂一些,并且混淆很可能产生 BUG。在对美国前 10 名的网站调查中发现,“精简”可以缩小源代码 21% 的体积,而“混淆”可以达到 25%。尽管“混淆”可以更大程度减少代码大小,但精简 JavaScript 的风险更小。
除了精简外部 JavaScript 和 CSS,内嵌的 <script> 和 <style> 代码块也可以(应该)精简。即使使用 Gzip 压缩过的 JavaScript 和 CSS,“精简”文件仍然可以减少 5% 以上的大小。随着使用的 JavaScript 和 CSS 大小的增加,精简代码将会获得更大的益处。
批注
无论是 JavaScript,还是 CSS,所有页面资源都是要从服务器下载的,它们当然是越小越好。所以往往,三方框架,比如 jQuery 等,都会提供脚本文件的正式版本(精简过的,如果你打开看一下,密密麻麻一坨)和它们的debug版本。
避免重定向
类别:content
“重定向”是通过 HTTP 状态码 301 和 302 完成的。下面是一个 301 响应的 HTTP 头:
HTTP/1.1 301 Moved Permanently
Location: http://example.com/newuri
Content-Type: text/html
浏览器会自动地把用户定向到 Location 中指定的 URL。所有重定向需要的信息位于头中。响应的内容可以是空的。无论是 301 响应,还是 302,它们都不会被缓存,除非增加一个额外的头选项,如 Expires 或者 Cache-Control,来指定可以被缓存。meta 标记和 JavaScript 是另一个实现重定向的方法,但是如果你必须要跳转,那最好的方法是使用标准的 3XX HTTP 状态码,这主要是为了确保“后退”按钮可以正确地使用。
需要记住的主要事情是,重定向会降低用户体验。在用户和 HTML 文档之间插入一个跳转,会延迟页面中所有元素的呈现,因为,在 HTML 文件被加载前,页面的任何东西都不会被呈现,组件也不会被下载。
经常发生,最浪费的重定向,也经常被网页开发者忽略。那就是,URL 缺少斜杠(/),可本应该有。例如,访问 http://astrology.yahoo.com/astrology 会导致 301 响应代码的跳转,连接应该是 http://astrology.yahoo.com/astrology/ (注意末尾的斜杠)。在 Apache 中,可以使用 Alias 或者 mod_rewrite,或者 DirectorySlash 检测来避免。
另一个经常使用重定向的情况是,把旧网站连接到新网站。其他情况如连接网站的不同部分,或根据一定条件(浏览器类型、用户帐号类型等)来引导用户。使用重定向连接两个网站很简单,只需要很少的代码。尽管该方法对开发者来说,减少了复杂度,但是缺降低了用户体验。一个可替代的方法是,如果两者在同一台服务器上,那么可以使用 Alias 和 mod_rewrite。如果是因为域名改变而使用重定向,那么可以结合 Alias 或 mod_rewrite,使用 CNAME(创建从一个域名指向另外一个域名的 DNS 记录)。
批注
这个是有切身体会的,尤其是使用客户端重定向(浏览器重定向)时,页面跳转的速度实在是有点慢。
删除重复脚本
类别:javascript
在一个页面中引用同一个 JavaScript 文件两次会影响性能。这种情况可能并不常见。在对于美国前 10 名网站的调查中显示,其中的两家存在重复引用脚本的情况。有两个主要因素增加一个页面中重复引用脚本的几率:团队规模和脚本数量。当发生这种情况时,重复引用脚本会创建不必要的 HTTP 请求,以及延缓脚本执行,从而损害性能。
这种不必要的 HTTP 请求发生在 Internet Explorer,而不会在 Firefox。在 Internet Explorer 中,如果包含引用一个外部脚本两次,并且它还不可缓存,那么在页面加载期间,它会产生两次 HTTP 请求。即使脚本可被缓存,当页面重新加载时,也会产生额外的 HTTP 请求。
除了产生多余的 HTTP 请求,多次检查脚本也会浪费时间。无论脚本是否可被缓存,在 Internet Explorer 和 Firefox 中都会发生多余的脚本执行。
一个避免意外地引用同一个脚本两次的方法是,在你的系统中,开发脚本管理模块。在 HTML 页面中包含脚本的常见方法是使用 <script > 标记:
<script type="text/javascript" src="menu_1.0.17.js"></script>
而在 PHP 中,通过创建名为 insertScript 方法:
<?php insertScript("menu.js") ?>
为了防止多次插入同一个脚本,该方法可以解决其他脚本问题,例如依赖检查,为脚本文件名添加版本号,以便使用 Expire 头。
配置 ETags
类别:server
“实体标记(Entity tags,ETags)”是 Web 服务器和浏览器用于确定,浏览器缓存中的组件与服务器的一个原始内容是否匹配的一种机制。“实体”就是“组件”:图像、脚本、CSS 等。添加 ETags 会提供一种验证实体的机制,这比最后修改日期 last-modified date 更加灵活。一个 ETags 是一个唯一标识一个特定版本组件的字符串。字符串必须用双引号括起来。服务器通过 ETag 响应头来指定组件的 ETag。
HTTP/1.1 200 OK
Last-Modified: Tue, 12 Dec 2006 03:03:59 GMT
ETag: "10c24bc-4ab-457e1c1f"
Content-Length: 12195
之后,如果浏览器要验证一个组件,那么它会使用 If-None-Match 头把 ETag 回传诶服务器。如果 ETags 匹配,那么会返回 304 HTTP 状态码,减少了 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
ETags 的问题在于,它们通常使用网站服务器的唯一的属性来构造。当浏览器从一台服务器上获得原始组件,之后,尝试验证另一台服务器上的组件时,ETags 就不会匹配。这种情况在使用集群服务器来处理请求的 Web 站点上相当普遍。默认情况下,Apache 和 IIS 都把数据嵌入入到 ETag 中,这样能显著减少在多服务器情况下,成功在一台服务器验证的几率。
Apache 1.3 和 2.x 的 ETag 格式是 inode-size-timestamp。即使一个给定的文件在多个服务器的相同目录,且具有相同的大小、权限、时间戳等,但它的 inode 仍然是不同的。.
IIS 5.0 和 IIS 6.0 的 ETags 具有类似的问题。IIS 的 ETag 格式是 Filetimestamp:ChangeNumber。ChangeNumber 是一个计数器,用来跟踪 IIS 配置的改变。不同的是,ChangeNumber 在一个 Web 站点的所有 IIS 都是相同的。
对于完全相同的组件,在不同的服务器上,由 Apache 和 IIS 产生的 ETags 不同。如果 ETags 不匹配,那么用户不会收到一个小而快的 304 响应;而是,会得到一个正常的 200 响应,并下载全部内容。如果你的网站只在一台服务器上,那不会有问题。但是如果你的网站架设在多个服务器,并且使用 Apache,或默认 ETag 配置的 IIS,那么用户获得页面会相对较慢,你的服务器负载较高,带宽消耗较大,代理不会有效地缓存你的内容。即使你的组件具有 Expires 头,无论用户何时点击“重载”或者“刷新”,都会发送一个有条件的 GET 请求。
如果你没有利用 ETag 提供的灵活验证模式,那么最好把 ETag 删掉。Last-Modified 头是基于组件时间戳来验证。删掉 ETag 会在响应和接下来的请求中减少 HTTP 头的大小。Microsoft Support article 文档描述如何删掉 ETag。在 Apache 中,只需要在配置文件中简单添加下面一行代码就可以:
FileETag none
批注
这种为资源的生成唯一标识的做法很常见。如果文件有改动,那么唯一标识一定不同。
使 Ajax 可缓存
类别:content
Ajax 被提到的一个好处是,由于它从服务器异步请求信息,能为用户提供即时反馈。然而,使用 Ajax 并不能保证用户不用等待那些返回的异步 JavaScript 和 XML 响应。在很多应用中,用户是否等待取决于如何使用 Ajax。例如,在一个基于 Web 的 Email 客户端中,用户必须等待 Ajax 请求的结果,返回符合查找条件的邮件信息。很重要的一点,“异步”并不意味着“即时”。
为了提高性能,优化 Ajax 响应很重要。提高 Ajxa 性能的最重要的方法是使响应可缓存,如“添加 Expires 和 Cache-Control 头”小节描述的。其他几条规则也适用于 Ajax:
- Gzip 组件
- 减少 DNS 查询
- 减小 JavaScript
- 避免重定向
- 配置 ETags
让我们看一个例子:一个 Web2.0 Email 客户端会使用 Ajax 自动下载该用户的地址簿。如果用户在上次使用 Email Web 应用程序后,没有修改地址簿,那么,如果用 Expire 或者 Cacke-Control 头设置 Ajax 可缓存,就可以直接从缓存中读取地址薄。必须告诉浏览器,是使用缓存中的地址薄,还是发送一个新的请求。这可以通过给地址薄的 Ajax URL 添加一个时间戳,指示用户最后修改地址簿的时间,例如,&t=11900241612。如果地址薄在上次读取后没有被修改过,那么时间戳不变,从浏览器的缓存中读取地址簿,以减少额外的 HTTP 请求。如果用户修改了地址薄,那么时间戳会确保新的 URL 与缓存中的响应不匹配,浏览器将请求新的地址簿。
即便你的 Ajxa 响应是动态生成的,哪怕它只应用于一个用户,那也应该把它们缓存起来。这样可以使你的 Web2.0 应用程序响应速度更快。
尽早强制地发送缓冲给客户端
类别:server
当用户请求一个页面时,无论如何后端都会花 200 到 500 毫秒,以便组织 HTML 页面。期间,浏览器会一直是空闲的,直到数据到达。在 PHP 中,你可以使用 flush() 方法,它允许你把部分 HTML 响应发送给浏览器,这样,浏览器就可以开始获取组件,同时后台处理 HTML 页面的剩余部分。这样做好处在后端繁忙,而前端空闲时最明显。
考虑强制发送的一个最好的地方是 HEAD 后,因为 HTML 头通常最容易生成,让你可以包含任何 CSS 和 JavaScript 文件,以便浏览器在开始并行下载的同时,后端仍然在处理。 例子:
... <!-- css, js -->
</head>
<?php flush(); ?>
<body>
... <!-- content -->
Yahoo! search 率先进行了研究,真实的用户测试表明了该技术的好处。
批注
不用等到全部内容都有了,再发给客户端,这期间用户都看不到。有的话,就立刻发出去,让页面“慢慢”呈现出来。
使用 GET 发送 Ajax 请求
类别:server
Yahoo!Mail 团队发现,当使用 XMLHttpRequest 时,浏览器中 POST 的实现是两步过程:首先发送头,然后才发送数据。这样,使用 GET 最好,因为,它只发送一个 TCP 包(除非你有很多 cookie)。IE URL 的最大长度为 2K,因此,如果你要发送一个超过 2K 的数据,那就不能使用 GET。
一个有趣的副作用是,没有真正发送任何数据的 POST 的行为有点像 GET。根据 HTTP 规范,GET 意味着“检索”数据,因此,当你只是查询数据时,GET 更加有意义(从语意上也是如此),相反,发送并在服务端保存数据时使用 POST。
延迟加载组件
类别:content
你可以仔细看一下你的网页,问问自己“哪些内容是页面初次呈现所必需的?”。剩下的内容和组件可以稍后加载。
JavaScript 是一个理想的选择,按照 onload 事件分成两部分,之前和之后。例如,如果你有用于完成拖拽效果的 JavaScript 和库,那么它们可以稍后加载,因为页面的拖拽元素是在页面初始呈现后才发生的。其他稍后加载的选择包括隐藏的内容(这些内容是用户操作后才出现的),以及处于折叠的图像。
帮你减轻该工作的工具:YUI Image Loader 可以让你推迟加载折叠部分的图片。YUI Get utility 是包含 JS 和 CSS 的便捷方法。例如,你可以用 Firebug 的 Net 选项卡看一下 Yahoo! Home Page。
当性能目标体现在 Web 开发的最佳实践时,就会有很好的效果。这种情况下,通过渐进增强(progressive enhancement )的思想告诉我们,在支持 JavaScript 的情况下,JavaScript 可以改进用户体验,但是必须确保页面没有JavaScript 也可以正常工作。因此,在确保页面运行正常后,用延迟加载脚本来增强页面,比如拖拽和动画脚本。
批注
刚开始只加载最基本,暂时不需要的组件,就不用加载。遵循“渐进增强”原则。
预加载组件
类别:content
“预加载”和“延迟加载”看似相反,但实际上“预加载”是为了实现另外一种目标。通过预加载,你可以利用浏览器空闲的时间,请求将来需要的组件(如图像、CSS 和脚本)。使用这种方法,当用户要访问下一个页面时,页面的大部分组件都已经在缓存中了,这会打打改善用户加载页面的速度。
下面是几种“预加载”的方法:
- 无条件的预加载:只要触发 onload 事件,你就直接获取额外的组件。以 Google.com 为例,看一下合成的图像是如何在 onload 中加载的。合成中的图像在 google.com 主页并不需要,但在一个“连续”的检索结果页面中是需要的。
- 有条件的预加载:根据用户的操作,你可以推测出用户接下来会做什么,进行相应的预加载。在 search.yahoo.com 中,你可以看到在你在文本框输入后,如何请求额外的组件。
- 期望的预加载:在重新设计前,应先考虑预加载。当重新设计后,你经常能听到:“新的站点和不错,但比之前慢了”。部分问题在于,用户在完全缓存里访问你的之前的站点,而新的站点一直是空的缓存。因此,即便要重新设计,你也要通过预加载减轻这种副作用。旧站点使用浏览器的空闲时间,请求新站点使用的图像和脚本。
批注
最明显的情况是,“翻转”效果。
减少 DOM 元素数量
类别:content
一个复杂的页面意味下载更多的数据,也就意味着 JavaScript 访问 DOM 会变慢。例如,当你添加一个事件处理时,遍历页面的 500 或 5000 个元素是不一样的。
大量的 DOM 元素是一个征兆,它意味着,可以使用页面标记,而无需删除内容。你是否布局而采用内置表格?是否仅仅为了自适应而使用很多 DIV?也许有一个更好,更符合语义的方法。
用 YUI CSS utilities 来布局很方便:grids.css 可以帮你整体布局,font.css 和 reset.css 可以帮助你移除浏览器默认的格式。这提供了一个重新审视和思考标记的机会,例如,只有当在语义上说得通时才使用<div>,而不是因为它能呈现一个新行才使用。
DOM 元素数量很容易计算,只需要在 Firebug 控制台内输入:
document.getElementsByTagName('*').length
那么多少个 DOM 元素算多?可以对比一下好的页面。比如 Yahoo! Home Page 是一个内容很多的页面,但它只有 700 个元素(HTML 标签)。
批注
页面好看与页面复杂总是很矛盾。够炫的页面往往都很复杂,元素很多,元素很多的话,JavaScript 访问起来就会变慢。
根据域名分割组件
类别:content
分割组件可以使你最大限度地并行下载。由于 DNS 查找的影响,确保你使用的域名在2到4个之间。例如,你可以把 HTML 和动态内容放在 www.example.org 上,而把分割的静态组件(图片、脚本、CSS)放在 statics1.example.org 和 statics.example.org。
你可以在 Tenni Theurer 和 Patty Chi 合写的文章 "Maximizing Parallel Downloads in the Carpool Lane" 找到更多相关信息。
批注
既然在 HTML 规范中规定资源是可以并行下载的,那么我们当然可以将网站的资源分别存在不同的地方。
最小化 iframe 数量
类别:content
ifrmae 元素可以在父文档中插入一个新的 HTML 文档。了解 iframe 如何工作,才能有效地使用它。
<iframe> 优点:
- 帮助加载缓慢的三方部件和广告等
- Security sandbox
- 并行下载脚本
<iframe> 缺点:
- 即便加载是空的,也有代价
- 会阻止页面加载
- 没有语意
不要出现 404 错误
类别:content
HTTP 请求很昂贵。因此,发送一个 HTTP 请求,却获得一个无用的响应(如,404 Not Found)是完全没必要的,它只会降低用户体验,而不会有一点好处。
有些站点把 404 错误响应页面改为“你是不是要找***?”,这虽然改进了用户体验,但却浪费了服务器资源(像数据库等)。最糟糕的情况是指向一个外部 JavaScript 链接,返回 404 错误。首先,这个下载会阻塞并行下载。其次,浏览器会试图解析 404 响应的内容,就像它是 JavaScript 代码,尝试在里边查找有用的东西。
减小 Cookie 大小
类别:cookie
使用 HTTP coockie 有很多原因,比如认证(authentication )和个性化。在 Web 服务器与浏览器之间,通过 HTTP 头来交换 coockie 信息。尽可能维持 coockie 的大小,以减少对用户响应时间的影响,很重要。
有关更多信息,可以查看 Tenni Theurer 和 Patty Chi 的文章 "When the Cookie Crumbles"。该文内容的研究包括:
- 去掉不必要的 coockie
- 尽可能维持 coockie 的大小,以减少对用户响应时间的影响
- 注意在适当域名级别上设置 coockie,以便其他子域不会受到影响
- 适当设置的过期时间。删除 cookie 较早的过期时间或没有,能改善用户的响应时间。
对组件使用无 coockie 域名
类别:cookie
当浏览器请求一个静态图像,并且随请求发送 coockie 时,服务器并不会使用这些 coockie。因此,毫无疑问,它们(coockie )只会产生网络流量。你应该确保请求静态组件时,请求中不带 cookie。创建一个域,把你所有的静态组件放在该子域。
如果你的域名是 ww.example.org,那么你可以把静态组件放在 static.example.org 上。然而,如果你已经在顶级域 example.org 设置了 coockie,而不是在 www.example.org 上,那么,所有对 static.example.org 的请求都将包含 coockie。在这种情况下,你可以购买一个新域名,存放你的静态组件,并让这个域名无 coockie。Yahoo! 使用的是 ymig.com,YouTube 使用的是 ytimg.com,Amazon 使用 images-anazon.com 等。
使用无 coockie 域名存放静态组件的另外一个好处是,一些代理(服务器)可能会拒绝缓存带 coockie 请求的组件。相关建议是,如果你想确定是用 example.org 作为你的主页,还是 www.example.org,那么,你可以考虑 coockie 带来的影响。没有 www 的会把 coockie 设置到 *.example.org 的所有域,这样你就别无选择了。因此,出于性能的考虑,最好使用 www 子域名,并在它上设置 coockie。
最小化 DOM 访问
类别:javascript
用 JavaScript 访问 DOM 元素比较慢,因此为了更好响应页面,你应该:
- 缓存已经访问过的原始
- 离线更新完节点后,再将它们添加到文档树中
- 避免使用 JavaScript 来自适应布局
有关更多信息,请查看 Julien Lecomte 的文章 "High Performance Ajax Applications"。
开发智能事件处理程序
类别:javascript
有时,页面反应迟钝,这是因为,太多绑定到 DOM 树元素的事件处理,并且被频繁执行。这就是为什么使用事件托管(event delegation)。如果你在一个 div 中有 10 个按钮,那么你只需在 div 上绑定一个事件处理(利用委托),而不是为每个按钮。事件冒泡时,你可以捕捉到该事件,并判断出是哪个事件发出的。
你也不用为了操作 DOM 树而等待 onload 事件。通常,你所需要就是访问DOM 树中可用的元素。你也不必等待所有图像都加载完毕。不用 onload,DOMContentLoaded 是可以考虑的事件,但在所有浏览器都支持它之前,你可使用 YUI Event 工具,它有一个 onAvailable 方法。
有关更多信息,参看 Julien Lecomte 的文章 "High Performance Ajax Applications"。
批注
换句话说,通过事件委托,我们可以为 10 个按钮只使用一个事件,而不是 10 个按钮搞 10 个事件处理。比如,增删改三个按钮,可以用一个事件来弄,事件触发时,可以知道当前操作的是什么。
用 <link> 代替 @import
类别:css
前面的最佳实践提到,CSS 应该放在页面顶部,以便渐进呈现。
在 IE 中,@import 的行为相当于把 <link> 放在页面顶部,因此最好不要使用它。
避免使用滤镜
类别:css
IE 独有的 AlphaImageLoader 滤镜旨在修复 7.0 以下版本的半透明真彩色 PNG 图像问题。该滤镜的问题是,当图像正被下载时,它会阻塞呈现,并冻结浏览器。滤镜也会增加内存消耗,并被应用到每个元素,而不是每个图像,因此,滤镜的问题是多方面的。
最好的方法是避免完全使用 AlphaImageLoader,而是使用 PNG8,它能在 IE 中很好地工作。如果你确实需要使用 AlphaImageLoader,那应该使用 hack_filter,不会影响到 IE7 以上版本的用户。
优化图像
类别:images
设计人员为页面创建图像后,向 Web 服务器上传图像前,你可以试着做以下几件事:
- 检查 GIF 图像,看下图像中的颜色数量是否与调色板一致。使用 imagemagick 可以很容易检查:
identify -verbose image.gif
如果你发现图像中只用了 4 种颜色,而调色板中是 256 色,那么这张图片就改进的空间。
- 尝试把 GIF 格式转换成 PNG 格式,看看是否节省了空间。多数情况下会。早先由于浏览器支持有限,开发者不太愿意使用 PNG 格式,但现在已经成为过去。唯一一个问题是真彩 PNG 格式的 alpha 通道半透明问题,不过 GIF 也不是真彩的,并且不支持任何半透明。因此,GIF 能做到的,调色板 PNG(PNG8)同样也能做到(除了动画)。下面一条简单的 imagemagick 命令可以把 GIF 安全地完全转换成 PNG:
convert image.gif image.png
- 在你所有的 PNG 图像上,运行 pngcrush(或者其它 PNG 优化工具)。例如:
pngcrush image.png -rem alla -reduce -brute result.png
- 在你所有的 JPEG 图像上运行 jpegtran。这个工具可以对 JPEG 中出现的锯齿等做无损操作,同时,它还可以用于优化和清除图像中的注释,以及其它无用信息(如 EXIF 信息):
jpegtran -copy none -optimize -perfect src.jpg dest.jpg
优化 CSS Sprites
类别:images
- 通常,在 Sprite 中水平排列图像比垂直排列的文件要小。
- 在一个 Sprite 中合并结合相似的颜色会帮助你保持较低的色彩数,理想状态是 256 色,以适应一个 PNG8。
- "Be mobile-friendly",并且不要在一个 Sprite 中的图像之间留下较大的空隙。这不会影响文件的大小,但对于迫切需要解压缩图像到一个像素地图上的用户来说,需要更少的的内存。100x100 的图像是1 万个像素,而 1000X1000 是 100 个万像素。
不要在 HTML 中缩放图像
类别:images
不要使用比你实际需要的大的图像。因为你可以设置长宽。如果你需要:
<img width="100" height="100" src="mycat.jpg" alt="My Cat" />
那么你的图像(mycat.jpg)就应该是 100x100 像素,而不是把一个 500x500 像素的图片缩小来用。
批注
使用多大的图像,就做多大的图像。你可能见过,往往一个内容,会有不同像素大小的版本,比如工具栏按钮图标,同时做 16×16 像素,和 32×32 像素的。
favicon.ico 要小且可缓存
类别:images
favicon.ico 是一个放在你服务器根位置的图像。这是个必需的邪恶,因为,即使你不关心它,浏览器仍然会请求它,因此,最好不要用 404 响应。此外,由于位于同一个服务器,每次发送 Cookie 都会请求它。这个图像也会干扰下载队列,例如,在 IE 中,当你在 onload 请求一个额外的组件时,favicon 将在这些额外组件前被下载。
因此,为了减轻 favicon.ico 弊端,应确保:
- favicon 要小,最好在 1K 以下。
- 设置你感觉合适的 Expires 头(若你想改变它,但你不能重新命名它)。你可以把 Expires 头设置为未来几个月。你可以检查当前 favicon.ico 的最后修改日期,以便做决定。
ImageMagick 可以帮助你创建小的 favicons(网站图例)。
批注
千万别忘了 favicon.ico。这是浏览器默认下载的资源。
保持组件 25K 以下
类别:mobile
这个限制与一个事实有关,iPhone 不能缓存操作 25K 大小的组件。注意,这是未压缩的大小。这个小是很重要的,因为单独 gzip 完全不够。
有关更多信息,参考 Wayne Shea 和 Tenni Theurer 的文章 "Performance Research, Part 5: iPhone Cacheability - Making it Stick"。
把组件打包到一个 Multipart 文档
类别:mobile
把组件打包到一个 multipart 文档很像一个带附件的email,它能使你在一个 HTTP 请求中获取多个组件(切记:HTTP 请求很昂贵)。当你使用这个技术时,首先要确定用户代理是否支持(iPhone 不支持)。
避免图片 src 属性为空
类别:server
img src 属性为空的经常发生。它有两种形式:
- 直接 HTML 标记创建
<img src="">
- JavaScript 代码创建
var img = new Image();
img.src = "";
这两种形式效果相同:浏览器会向服务器发出另一个请求。
- Internet Explorer 向页面所在的目录,发出一个请求。
- Safari 和 Chrome 向实际页面自身,发出一个请求。
- Firefox 3 和更早版本的行为与 Safari 和 Chrome 相同,但其 3.5 版本解决了这个问题[bug 444931],不再发送请求。
- Opera 当遇到img src 属性为空时,什么都不做。
为什么这个行为很糟?
- 发送大量不期望的流量,会使服务器瘫痪,尤其是每天有百万浏览的页面。
- 浪费服务器的处理周期,却产生一个永远不会被查看的页面。
- 可能会破坏用户数据。如果你通过 cookie或其他方式追踪请求中的状态,那么会有损坏数据的可能性。即使图像请求没有返回图像,浏览器也会读取和接收所有的头,包括所有 cookies。当余下的响应被丢弃,损害可能已经产生。
这种行为的根本原因是在浏览器中完成 URI 解析的方式。这种行为定义在 RFC 3986 - Uniform Resource Identifiers。当遇到一个空字符串时就作为 URI,它被看作是一个相对的 URI,并根据 5.2 节定义的算法解决。在 5.4 节 列出了空字符串的例子。Firefox、Safari 和 Chrome 都按规范正确解析字符串,而 Internet Explorer 没有,还遵循早期的规范 RFC 2396 - Uniform Resource Identifiers(已由 RFC 3986 代替)。因此,技术上,浏览器做了它们被期望做的事,来解析相对 URI。空字符串显然是个意外。
HTML5 在 4.8.2 节添加一个关于标记的 src 属性的描述,指示浏览器不要发出额外的请求:
src 属性必须存在,并且必须包含一个有效的 URL,它引用一个非交互式的、可选的、可有动画的图像源,不能是页面或脚本。如果元素的基 URI 与文档地址相同,那么 src 属性必须不能为空。
庆幸的是,之后的浏览器不会存在这个问题。而不幸的是,没有对 <script src=""> 和 <link href=""> 的规定。可能还是有时间调整以确保浏览器执行此行为。
该规则得到 Yahoo!'s JavaScript guru Nicolas C. Zakas 的支持。有关更多信息,参考 "Empty image src can destroy your site"。
修改记录
- 第一次 2011-12-29