高性能网站建设指南

  • 性能黄金法则(Performance Golden Rule)揭示了只有10%20%的最终用户响应时间花在接收所请求的HTML文档上。剩下的80%90%时间花在为HTML文档所引用的所有组件(图片、脚本、样式表、Flash等)进行的HTTP请求上。

减少HTTP请求

用户在第一次访问你的网站时能更有效地减少HTTP请求的数量。

图片地图 Image Maps

图片地图允许你在一个图片关联多个URL,目标URL的选择取决于用户单击了图片上的哪个位置。

图片地图有两种类型:

  • 服务端图片地图(Server-side image maps)
  • 客户端图片地图(Client-side image maps) 映射通过HTML的map标签实现

CSS Sprites

通过合并多张图片为一个,然后使用CSS的background-position属性,可以将HTML元素放置到背景图片中期望的位置上。

CSS Sprites还降低了图片下载量。很多人会认为合并后的图片会比分离的图片的总和要大,因为合并的图片中包含附件的空白区域。实际上,合并的图片会比分离的图片的总和要小,这是因为它降低了图片自身的开销(颜色表、格式信息、等等)。

内联图片 Inline Images

通过使用data:URL模式可以在Web页面中包含图片但无需任何额外的HTTP请求。尽管IE目前还不支持这种方式,但它能给其他浏览器带来的节省使得它值得关注。

缺陷在于浏览器支持和数据大小上的限制。另外,Base64编码会增加图片的大小。

合并脚本和样式表

一个页面可能需要script1、script2和script3,而另一个页面可能需要script1、script3、script4。解决的方法是遵守编译型语言的模式,保持JavaScript的模块化,在生成过程中从一组特定的模块生成一个目标文件。

使用内容发布网络

CDN用于发布静态内容,如图片、脚本、样式表和Flash。提供动态HTML页面会引入特殊的存储需求——数据库连接、状态管理、验证、硬件和OS优化等。这些复杂性超越了CDN的能力范围。另一方面,静态文件更容易存储并具有较少的依赖性。

添加Expires头

HTTP1.1引入了Cache-Control头来克服Expires头的限制。因为Expires头使用一个特定的时间,它要求服务器和客户端的时钟严格同步。另外,过期日期需要经常检查,并且一旦未来这一天到来了,还需要在服务器配置中提供一个新的日期。换一种方式,Cache-Control使用max-age指令指定组件被缓存多久。它以秒为单位定义了一个更新窗。如果从组件被请求开始过去的秒数少于max-age,浏览器就使用缓存的版本,这就避免了额外的HTTP请求。如果这两者同时出现,HTTP规范规定max-age指令将重写Expires头。

压缩组件

从HTTP1.1开始,Web客户端可以通过HTTP请求中的Accept-Encoding头来标识对压缩的支持。

1
Accept-Encoding: gzip,deflate

一般支持gzip的不一定支持deflate,但支持deflate的一定支持gzip。一般来说,gzip比deflate能多压缩6%。
如果Web服务器看到请求中有这个头,就会使用客户端列出来的方法中的一种来压缩响应。Web服务器通过响应中的Content-Encoding头来通知Web客户端。

1
Content-Encoding: gzip

压缩的成本有——服务器端会花费额外的CPU周期来完成压缩,客户端要对压缩文件进行解压缩。

将样式表放在顶部

进度指示器有三个主要优势——它们让用户知道系统没有崩溃,只是正则为他或她解决问题;它们指出了用户大概还需要等多久,以便用户能够在漫长的等待中做些其他事情;最后,它们能给用户提供一些可以看的东西,使得等待不再是那么无聊。

将样式表放在文档底部会导致在浏览器中阻止内容逐步呈现。为避免当样式变化时重绘页面中的元素,浏览器会阻塞内容逐步呈现。“将样式表放在顶部”这条规则对于加载页面所需的实际时间没有太多影响,它影响更多的是浏览器对这些组件顺序的反应。实际上,用户感觉缓慢的页面反而是可视化组建加载得更快的页面。在浏览器和用户等待位于底部的样式表时,浏览器会延迟显示任何可视化组件。

无样式内容的闪烁 Flash of Unstyled Content

如果样式表仍在加载,构建呈现树就是一种浪费,因为在所有样式表加载并解析完毕之前无需绘制任何东西。否则,在其准备好之前显示内容会遇到FOUC(无样式内容的闪烁)问题。

白屏是浏览器在尝试修改前端工程师所犯的错误——将样式表放在文档比较靠后的位置。白屏是对FOUC问题的弥补。浏览器可以延迟呈现,直到所有的样式表都下载完之后,这就导致了白屏。

将脚本放在底部

并行下载

对响应时间影响最大的是页面中组件的数量。当缓存为空时,每个组件都会产生一个HTTP请求,有时即便缓存是完整的亦是如此。HTTP1.1规范建议浏览器从每个主机名并行地下载两个组件。

今天的很多网站使用HTTP1.1,但将并行下载数增加到每个主机名超过两个也是有可能的。前端工程师与其依赖用户来修改浏览器设置,不如简单地使用CNAME(DNS别名)来将组件分别放到多个主机名中。但增加并行下载数量并不是没有开销的,其优劣取决于你的带宽和CPU速度,过多的并行下载反而会降低性能。Yahoo!的研究表明,使用两个主机名比使用1、4或10个主机名能带来更好的性能。

脚本阻塞下载

并行下载组件的有点是很明显的。然而,在下载脚本时并行下载实际上是被禁用的——即使使用了不同的主机名,浏览器也不会启动其他的下载。其中一个原因是,脚本可能使用document.write来修改页面内容,因此浏览器会等待,以确保页面能够恰当地布局。

在下载脚本时浏览器阻塞并行下载的另外一个原因是为了保证脚本能够按照正确的顺序执行。如果并行下载多个脚本,就无法保证响应是按照特定顺序到达浏览器的。例如,后面的脚本比页面中之前出现的脚本更小,它可能首先执行。如果它们之间存在着依赖关系,不按顺序执行就会导致JavaScript错误。

如果将脚本放在顶部,脚本会阻塞对其后面内容的呈现、对其后面组件的下载。

避免CSS表达式

表达式的问题在于对其进行的求值的频率比人们期望的要高。它们不止在页面呈现和大小改变时求值,当页面滚动、甚至用户鼠标在页面上移过时都要求值。

有两种技术可以避免CSS表达式产生这一问题——创建一次性表达式和使用事件处理器取代CSS表达式。

使用外部JavaScript和CSS

使用外部JS和CSS,关键是外部文件所带来的收益——JavaScript和CSS文件有机会被浏览器缓存起来。HTML文档——至少是那些包含动态内容的HTML文档——通常不会被配置为可以进行缓存。另一方面,如果JS和CSS是外部文件,浏览器就能缓存他们,HTML文档的大小减小,而且不会增加HTTP请求的数量。

关键因素是,与HTML文档请求数量相关的、外部JavaScript和CSS组件被缓存的频率。这可通过以下衡量:

  • Page View
  • 空缓存与完整缓存
  • 组件重用

两全其美的方法

  • 加载后下载
  • 动态内联

减少DNS查找

DNS也是开销。通常浏览器查找一个给定的主机名的IP地址要花费20~120毫秒。在DNS查找完成之前,浏览器不能呢个从主机名那里下载到任何东西。响应时间依赖于DNS解析器(通常由你的ISP提供)、它所承担的请求压力、你与它之间的距离和你的带宽速度。

DNS缓存和TTL

DNS查找可以被缓存起来以提高性能。这种缓存可以发生在由你的ISP或局域网中的一台特殊的缓存服务器上。但我们这里要探索的是发生在独立用户的计算机上的DNS缓存;在用户请求了一个主机名之后,DNS信息会留在操作系统的DNS缓存中(Microsoft Windows上的“DNS Client服务“),之后对该主机名的请求将无需进行过多的DNS查找,至少短时间内不需要。

很多浏览器拥有其自己的缓存,和操作系统的缓存相分离。只要浏览器在其缓存中保留了DNS记录,它就不会麻烦操作系统来请求这个记录。只有当浏览器缓存丢弃了记录,它才会向操作系统询问地址——然后操作系统或者通过其缓存来相应这个请求,或者将请求发送给一台远程服务器,这时就会发生潜在的速度降低。

影响DNS缓存的因素

服务器可以表明记录可以被缓存多久。查找返回的DNS记录包含了一个存活时间(Time-to-live,TTL)值。该值告诉客户端可以对该记录缓存多久。

尽管操作系统缓存会考虑TTL值,但浏览器通常忽略该值,并设置它自己的时间限制。

浏览器对缓存的DNS记录的数量也有限制,而不管缓存记录的时间。

TTL值短的原因一般是拥有巨大数量用户的顶级网站在努力做到当服务器、虚拟IP地址(VIP)或联合定位掉线时提供快速故障转移。如果长的话,一般是因为定位到一个联合定位工具,对于其当前的网络拓补,故障转移并不是那么重要,因此较长的TTL可以减少DNS查找,同时也降低了其名称服务器的负载。

减少DNS查找

当客户端的DNS缓存为空(浏览器和操作系统都是)时,DNS查找的数量与Web页面中唯一主机名的数量相等。这包括页面URL、图片、脚本文件、样式表、Flash对象等的主机名。减少唯一主机名的数量就可以减少DNS查找的数量。

减少唯一主机名的数量会潜在地减少页面中并行下载的数量。避免DNS查找降低了响应时间,但减少并行下载可能会响应时间。建议是至少2个,但不要超过4个主机名下。

使用Keep-Alive所带来的好处在于,它可以通过重用现有连接,从而通过避免TCP/IP开销来减少响应时间。确保服务器支持Keep-Alive还能减少DNS查找。

通过使用Keep-Alive和较少的域名来减少DNS查找。

精简JavaScript

精简 Minification

精简是从代码中移除不必要的字符以减小其大小。

混淆 Obfuscation

混淆除了精简外,还会改写代码。函数和变量的名字将被转换为更短的字符串,这样做是为了增加对代码进行反向工程的难度。

混淆JavaScript有以下缺点:

  • 缺陷

由于混淆更加复杂,混淆过程本身很有可能引入错误。

  • 维护

由于混淆会改变JavaScript符号,因此需要对任何不能改变的符号(例如API函数)进行标记,防止混淆器修改它们。

  • 调试

经过混淆的代码很难阅读。这使得在产品环境中调试问题更加困难。

在结合使用了gzip压缩之后,精简和混淆之间的差距将会减小。精简脚本可以降低响应时间,但不会带来混淆的风险。

精简CSS

精简CSS能够带来的节省通常要小于精简JS。最大的潜在节省来自于优化CSS。

避免重定向

重定向引起的延迟很严重,因为它延迟了整个HTML文档的传输。

重定向用于将用户从一个URL重新路由到另一个URL。重定向有很多种——301和302是最常用的两种。通常针对HTML文档进行重定向,但通常也可能用在请球页面中的组件时。实现重定向可能有很多不同的原因,包括网站重新设计、跟踪流量、记录广告点击和简历易于记忆的URL。

重定向会使你的页面变慢。

  • 300 Multiple Choices(基于Content-Type)
  • 301 Moved Permancently
  • 302 Moved Temporarily
  • 303 See Other(对302的说明)
  • 304 Not Modified (并不是真的重定向,对GET请求的回应)
  • 305 Use Proxy
  • 306 (不再使用)
  • 307 Temporary Redirect(对302的说明)

状态码301和302是使用得最多的。状态码303和307是在HTTP1.1规范中添加的,用来澄清对302的使用(滥用),但几乎没有人用它们,绝大多数网站仍然在沿用302。

重定向之外的其他选择

  • 解决缺少结尾斜线的问题

  • 连接网站

Alias、mod_rewrite、DirectorySlash和直接链接代码来避免重定向。

移除重复脚本

重复脚本损伤性能——不必要的HTTP请求和执行JavaScript所浪费的时间。

  • 在页面中多次包含相同的脚本会使页面变慢。

  • 在IE中,如果脚本没有被缓存,或在重新加载页面时,会产生额外的HTTP请求。

  • 在Firefox和IE中,脚本会被多次求值。

配置或移除ETag

ETag的问题在于,对于服务器集群来处理请求的网站来说,ETag在不同服务器是不匹配的。默认情况下,对于拥有多台服务器的网站,Apache和IIS向ETag中嵌入的数据都会大大地降低有效性验证的成功率。

从ETag中移除ChangeNumber或完全移除ETag可以避免当数据已经位于浏览器缓存中时进行不必要的和低效的下载。

使Ajax可缓存

确保Ajax请求遵守性能知道,尤其应具有长久的Expires头。

本篇文章参考《高性能网站建设指南》
提到的优化方法参见http://stevesouders.com/hpws/rules.php