acm-header
登录

ACM通信

实践

高性能的网站


插图:尼克·舒尔茨

谷歌地图,Yahoo !Mail、Facebook、MySpace、YouTube和Amazon都是按规模构建的网站。他们访问pb级的数据,以每秒太比特的速度发送给全球数百万用户。其规模之大令人惊叹。

用户从更狭窄的角度来看待这些大型Web站点。典型的用户有兆字节的数据,他们以每秒几百千比特的速度下载。用户对每秒处理的大量请求不太感兴趣,他们更关心的是他们自己的请求。当他们使用这些Web应用程序时,他们不可避免地会问同样的问题:“为什么这个网站这么慢?”

答案取决于开发团队将性能改进的重点放在哪里。为了可伸缩性,性能正确地集中在后端。数据库调优、复制体系结构、自定义数据缓存等都允许Web服务器处理更多的请求。这种效率的提高转化为硬件成本、数据中心机架空间和功耗的降低。但是后端在延迟方面对用户体验有多大影响呢?

这里列出的Web应用程序是世界上优化程度最高的一些应用程序,但是它们的加载时间仍然比我们希望的要长。似乎高速存储和后端优化的应用程序代码对最终用户的响应时间几乎没有影响。因此,要考虑到这些加载缓慢的页面,我们必须关注后端以外的东西:我们必须关注前端

回到顶部

前端表现的重要性

图1说明了当浏览器访问iGoogle时缓存为空时发送的HTTP流量。每个HTTP请求由一个水平条表示,其位置和大小基于请求开始的时间和花费的时间。第一个HTTP请求是针对HTML文档(http://www.google.com/ig)的。正如图1,对HTML文档的请求只占整个页面加载时间的9%。这包括将请求从浏览器发送到服务器的时间,服务器在后端收集所有必要的信息并将其拼接成HTML的时间,以及将HTML发送回浏览器的时间。

剩下的91%花在前端,其中包括HTML文档命令浏览器执行的所有操作。其中很大一部分是获取资源。对于这个iGoogle页面,有22个额外的HTTP请求:两个脚本、一个样式表、一个iframe和18张图片。HTTP配置文件中的空白(没有网络流量的地方)是浏览器解析CSS、解析和执行JavaScript的地方。

iGoogle的引物缓存情况如图所示图2.这里只有两个HTTP请求:一个用于HTML文档,另一个用于动态脚本。差距甚至更大,因为它包括从磁盘读取缓存资源的时间。即使在启动缓存的情况下,HTML文档也只占整个页面加载时间的17%。

在这种情况下,大部分页面加载时间都花在前端,这适用于大多数Web站点。表1显示在美国排名前10的网站中有8个(在Alexa.com上列出),终端用户从后端获取HTML文档的响应时间不到20%。两个例外是谷歌搜索和Live搜索,它们是高度调优的。这两个站点在空缓存的情况下下载了四个或更少的资源,并且只有一个请求使用了primed缓存。

生成HTML文档所花费的时间会影响整体延迟,但是对于大多数Web站点来说,这个后端时间与前端所花费的时间相比微不足道。如果目标是让用户体验更快,那么应该关注前端。鉴于这个新的重点,下一步是确定改进前端性能的最佳实践。

回到顶部

前端性能最佳实践

通过研究和与开发团队的咨询,我开发了一组已被证明可以加快Web页面的性能改进。超级粉丝哈维·佩尼克的红宝书1我的建议是“Take Dead Aim”,我打算把这些最佳做法总结成一个简单的清单,便于记忆。这个列表包含了以下14条优先排序规则:

  1. 减少HTTP请求
  2. 使用内容传递网络
  3. 添加过期报头
  4. Gzip部件
  5. 将样式表放在顶部
  6. 把脚本放在底部
  7. 避免CSS表达式
  8. 将JavaScript和CSS置于外部
  9. 减少DNS查找
  10. 贬低JavaScript
  11. 避免重定向
  12. 去除重复的脚本
  13. 配置etag
  14. 使Ajax缓存

对每条规则的详细解释是我这本书的基础,高性能网站2下面是每条规则的简要摘要。

回到顶部

规则1:减少HTTP请求

随着页面中的资源数量的增加,整个页面的加载时间也会增加。正如HTTP/1.1规范(http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.1.4)所建议的那样,大多数浏览器一次只从给定的主机名下载两个资源,这加剧了这种情况。一个有几种技术可以在不减少页面内容的情况下减少HTTP请求的数量:

  • 将多个脚本合并为单个脚本。
  • 将多个样式表合并为单个样式表。
  • 将多个CSS背景图像合并成一个称为CSS sprite的图像(参见http://alistapart.com/articles/sprites)。

回到顶部

规则2:使用内容传递网络

内容传递网络(CDN)是一组分布式Web服务器,用于更有效地向用户传递内容。例子包括Akamai Technologies、Limelight Networks、SAWIS和Panther Express。CDN提供的主要性能优势是从地理上更接近最终用户的服务器交付静态资源。其他好处包括备份、缓存和更好地吸收流量峰值的能力。

回到顶部

规则3:添加过期头

当用户访问Web页面时,浏览器下载并缓存该页面的资源。下次用户访问页面时,浏览器检查是否有资源可以从它的缓存中提供服务,从而避免了耗时的HTTP请求。浏览器根据资源的过期日期做出决定。如果有一个过期日期,并且这个日期是将来的,那么就从磁盘读取资源。如果没有过期日期,或者过期日期已经过去,浏览器就会发出代价高昂的HTTP请求。Web开发人员可以通过在未来指定一个明确的过期日期来获得这种性能增益。这是通过Expires HTTP响应头来完成的,如下所示:

有效期:2015年1月1日(星期四)格林尼治标准时间20:00:00

回到顶部

规则4:Gzip组件

通过网络传输的数据量影响响应时间,特别是对于网络连接较慢的用户。几十年来,开发人员一直使用压缩来减小文件的大小。同样的技术也可用于减少通过因特网发送的数据的大小。在默认情况下,许多Web服务器和Web托管服务都支持HTML文档的压缩,但压缩不应该止步于此。开发人员还应该压缩其他类型的文本响应,例如脚本、样式表、XML、JSON等等。Gzip是最流行的压缩技术。它通常会减少70%的数据大小。

回到顶部

规则5:把样式表放在顶部

样式表通知浏览器如何格式化页面中的元素。如果样式表包含在页面的下方,那么问题来了:浏览器应该如何处理在下载样式表之前可以呈现的元素?

Internet Explorer使用的一种解决方法是延迟呈现页面中的元素,直到下载所有样式表。但这会导致页面在较长时间内显示为空白,给用户一种页面运行缓慢的印象。Firefox使用的另一种方法是呈现页面元素并在样式表更改初始格式时重新绘制它们。这会导致页面中的元素在重绘时“闪烁”,这对用户来说是破坏性的。最好的答案是避免在页面下方包含样式表,而是在文档的HEAD中加载它们。

回到顶部

规则6:将脚本放在底部

外部脚本(通常是“.js”文件)对性能的影响比其他资源更大,原因有二。首先,一旦浏览器开始下载一个脚本,它就不会启动任何其他的并行下载。其次,在脚本完成下载之前,浏览器不会呈现脚本下面的任何元素。当脚本被放置在页面顶部附近时,例如在HEAD部分,就会感受到这两种影响。页面中的其他资源(如图像)被延迟下载,页面中已经存在的元素(如文档本身中的HTML文本)直到前面的脚本完成才会显示出来。将脚本移到页面下方可以避免这些问题。

回到顶部

规则7:避免使用CSS表达式

CSS表达式是一种在Internet Explorer中动态设置CSS属性的方法。它们支持根据执行嵌入在样式声明中的JavaScript代码的结果设置样式的属性。CSS表达式的问题是它们的计算频率比预期的要高——在单个页面加载期间可能要计算数千次。如果JavaScript代码是低效的,它会导致页面加载更慢。

回到顶部

规则8:让JavaScript和CSS成为外部的

JavaScript可以作为内联脚本添加到页面:

<脚本type = " text / javascript”>
< var foo =“酒吧”;
> < /脚本

或作为外部脚本:

< script src = " foo.js " type = " text / javascript " > < /脚本>

类似地,CSS被包含为内联样式块或外部样式表。从性能的角度来看,哪个更好?

HTML文档通常不会被缓存,因为它们的内容是不断变化的。JavaScript和CSS没有那么动态,通常几周或几个月都不会改变。内联JavaScript和CSS会在每个页面视图上下载相同的字节(没有改变)。这对响应时间有负面影响,并增加了从数据中心使用的带宽。对于大多数Web站点来说,最好是通过外部文件提供JavaScript和CSS,同时使用规则3中解释的远未来Expires头使它们可缓存。

回到顶部

规则9:减少DNS查找

域名系统(DNS)类似于电话簿:它将主机名映射到IP地址。主机名更容易理解,但IP地址是浏览器建立到Web服务器的连接所需要的。Web页面中使用的每个主机名都必须使用DNS进行解析。这些DNS查找是有成本的;它们每个可以花费20100毫秒。因此,最好减少Web页面中使用的惟一主机名的数量。

回到顶部

规则10:最小化JavaScript

正如规则4所述,压缩是减少通过Internet传输的文本文件大小的最佳方法。通过减少代码,可以进一步减少JavaScript的大小。缩小是从代码中去掉不需要的字符(注释、制表符、新行、额外的空白等等)的过程。最小化通常会使JavaScript的大小减少20%。外部脚本应该被缩小,但是内联脚本也从这种缩小中受益。

回到顶部

规则11:避免重定向

重定向用于将用户从一个URL映射到另一个URL。当真正的URL太长或太复杂,用户无法记住,或者URL发生了变化时,它们很容易实现,也很有用。缺点是重定向在用户和她的内容之间插入了额外的HTTP往返。在许多情况下,重定向可以通过一些额外的工作来避免。如果一个重定向确实是必要的,确保它发出一个远未来的Expires头(参见规则3),以便用户在以后的访问中可以避免这种延迟。b

回到顶部

规则12:删除重复的脚本

如果一个外部脚本多次包含在一个页面中,浏览器必须多次解析和执行相同的代码。在某些情况下,浏览器会多次请求该文件。这是低效的,会导致页面加载更慢。这个明显的错误似乎不常见,但在美国网站的回顾中,可以在前10名的网站中发现两个。拥有大量脚本和大量开发人员的Web站点最容易遇到这个问题。

回到顶部

规则13:配置ETags

实体标记(ETags)是Web客户机和服务器用来验证缓存资源是否有效的一种机制。换句话说,浏览器缓存中的资源(图像、脚本、样式表等)是否与服务器上的资源匹配?如果是这样,服务器不会(再次)传输整个文件,而是简单地返回一个304 Not Modified状态,告诉浏览器使用它的本地缓存副本。在HTTP/1.0中,有效性检查基于资源的Last-Modified日期:如果缓存文件的日期与服务器上的文件匹配,则验证成功。ETags是在HTTP/1.1中引入的,它允许基于其他信息的验证方案,比如版本号和校验和。

ETags并非没有成本。它们向HTTP请求和响应添加额外的报头。如果网站托管在多个服务器上,那么Apache和IIS中使用的默认ETag语法很可能导致验证错误失败。这些成本会影响性能,使页面变慢,增加Web服务器的负载。这是一种不必要的性能损失,因为大多数Web站点没有利用ETags的高级特性,而是依靠Last-Modified日期作为验证手段。默认情况下,在流行的Web服务器(包括Apache和IIS)中启用ETags。如果您的网站不使用ETags,最好在Web服务器中关闭它们。在Apache中,这可以通过向配置文件中添加“FileETag none”来实现。

回到顶部

规则14:使Ajax可缓存

许多流行的网站正在转向Web 2.0,并开始整合Ajax。Ajax请求涉及获取数据,这些数据通常是动态的、个性化的,或者两者兼有。在Web 1.0世界中,用户访问指定的URL并返回HTML文档就可以提供这些数据。因为HTML文档的URL是固定的(书签、链接等),所以有必要确保响应不被浏览器缓存。

Ajax响应不是这样的。Ajax请求的URL包含在HTML文档中;它没有书签或链接。开发人员在生成页面时可以自由更改Ajax请求的URL。这允许开发人员将Ajax响应设置为可缓存的。如果Ajax数据的更新版本可用,则通过向Ajax URL添加动态变量来避免缓存版本。例如,对用户地址簿的Ajax请求可以在URL中作为参数包含最后一次编辑的时间,“&edit=1218050433”。只要用户没有编辑他们的地址簿,就可以继续使用以前缓存的Ajax响应,从而使页面变得更快。

回到顶部

使用YSlow进行性能分析

推广这些性能最佳实践是一项挑战。我能够通过会议、培训班、咨询和文档来分享这些信息。即使掌握了这些知识,仍然需要花费数小时在包嗅探器中加载页面和读取HTML以确定适当的性能改进集。一个更好的选择是将这些专业知识编纂到一个任何人都可以运行的工具中,减少学习曲线并增加对这些性能最佳实践的采用。这就是YSlow的灵感来源。

YSlow (http://developer.yahoo.com/yslow/)是一个性能分析工具,它可以回答介绍中提出的问题:“为什么这个站点这么慢?”我创建了YSlow,以便任何Web开发人员都可以快速、轻松地将性能规则应用到他们的站点,并明确地发现哪些地方需要改进。它作为Firebug (http://getfirebug.com/)的扩展在Firefox中运行,后者是许多Web开发人员的首选工具。

的截图图3显示加载了iGoogle的Firefox。在窗口的下部打开了Firebug,其中有控制台、HTML、CSS、Script、DOM和Net选项卡。安装YSlow时,会添加“YSlow”页签。单击YSlow的Performance按钮,根据规则集对页面进行分析,得到页面的加权分数。

所示图3, YSlow解释了每条规则的结果,并详细说明了要修正什么。YSlow屏幕中的每条规则都是到配套网站的链接,在那里可以获得关于该规则的其他信息。

回到顶部

下一个性能挑战:JavaScript

Web 2.0为未来开发人员提供了一个可以提供类似桌面应用程序体验的Web应用程序。Web 2.0应用程序是使用JavaScript构建的,这带来了巨大的性能挑战,因为JavaScript会阻止浏览器中的下载和呈现。为了构建更快的Web 2.0应用程序,开发人员应该使用以下指导方针来解决这些性能问题:

  • 分割初始有效载荷
  • 不阻塞加载脚本
  • 不要分散脚本

回到顶部

分割初始有效载荷

Web 2.0应用程序只需要加载一个页面。与在Web 1.0中为用户请求的每个操作或信息加载更多页面不同,Web 2.0应用程序使用Ajax在幕后发出HTTP请求并适当更新用户界面。这意味着下载的一些JavaScript不会立即使用,而是提供用户将来可能需要的功能。问题是这个JavaScript子集阻塞了其他内容立即使用,为了将来可能永远不会使用的功能而延迟立即的内容。

表2显示,在美国排名前10的网站中,平均有74%的下载功能没有立即使用。为了利用这个机会,Web开发人员应该将他们的JavaScript有效负载分成两个脚本:立即使用的代码(~ 26%)和附加功能的代码(~74%)。第一个脚本应该像今天一样下载,但是由于它的大小减小了,初始页面将加载得更快。第二个脚本应该是延迟加载,这意味着在初始页面完全呈现之后,第二个脚本将使用下一节中列出的技术之一动态下载。

回到顶部

不阻塞加载脚本

正如“规则6:将脚本放在底部”中所描述的,外部脚本会阻止页面中其他内容的下载和呈现。当脚本以典型的方式加载时,这是正确的:

< scriptsrc = " foo.js " type = " text / javascript " > < /脚本>

但是有几种下载脚本的技术可以避免这种阻塞行为:

  • XHR eval
  • XHR注入
  • 脚本在iframe
  • 脚本DOM元素
  • 脚本推迟
  • 文档。写脚本标记

您可以在Cuzillion (http://stevesouders.com/cuzillion/)中看到这些技术,但是作为示例,让我们看看脚本DOM元素方法: