前言
最近看了《高性能网站建设指南》,本书详细介绍了雅虎团队在性能优化上的技术技巧和最佳实践。
虽然本书在08年就已经出版了,但是其解决方法的思路和原则放在今天,也是很值得学习的。
减少HTTP请求
在终端用户响应的时间中,有80%~90%时间用于下载HTML文档引用的所有组件。这部分时间包括下载页面中的图像、样式表、脚本、Flash等。因此,改善响应时间的最简单、也是最有效的途径就是减少组件的数量,并由此减少HTTP请求的数量。
减少页面组件数量的方法其实就是简化页面设计。那么有没有一种方法既能保持页面内容的丰富又能减少页面组件的数量?可以很容易想到的方法就是合并多个组件。
CSS Sprites (雪碧图)
雪碧图可以将多张图片合并成为一张图片,然后使用CSS的background-position
属性,将其设置到背景图片期望的位置上。
例如有个id为#nav
的导航栏,导航栏包含四个链接,每个链接被包围在一个LI
中,他们使用同一背景图片。每个LI
都有一个不同的类,通过background-position
属性指定了期望的偏移量。
ul#nav li { |
雪碧图已经被广泛使用,一般用在网站上的小图标这类,数量多、体积小、不常更新的图片上,例如淘宝首页的Sprites。它不仅降低了下载量,而且实际上,合并后的图片会比分离的图片的总和要小,这是因为它降低了图片自身的开销(颜色表、格式信息,等等)。
内联图片 (data:URL)
通过使用data:URL
模式可以在Web页面中包含图片但无需额外的HTTP请求。
规范中对它的描述为:
允许将小块数据内联为‘立即(immediate)数’
数据就在URL自身之中,格式为
data:[<mediatype>][;base64],<data> |
其实就是所谓的Base64图片格式。由于data:URL
是内联在页面中的,所以在跨越不同页面时不会被缓存(document一般不设置缓存)。
所以,更聪明的做法是使用CSS并将内联图片作为背景,并将该CSS作为外部样式表引用,这样内联图片就能缓存在样式表中了。
合并脚本和样式表
一个页面会引入多个脚本或者样式表,如果可以将这些单独的文件合并到一个文件中,可以减少HTTP请求的数量并缩短最终用户的响应时间。
在理想情况下,一个页面应该使用不多于一个的脚本和样式表。
然而在实际的开发环境中是很难完成的。在大型的、复杂的Web应用中,我们需要使用JavaScript的模块化的思想,将所有东西合并到一个单独的文件中看起来就是一种倒退。因此,解决的方法是遵守编译型语言的模式,保持JavaScript的模块化,而在生成过程中从一组特定的模块生成一个目标文件。
使用内容发布网络(CDN)
用户与你网站服务器的接近程度会影响响应时间的长短。把你的网站内容分散到多个、处于不同地域位置的服务器上可以加快下载速度。
内容发布网络(CDN)是一组分布在多个不同地理位置的Web服务器,用于更加有效地向用户发布内容。CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。
CDN使用户可以就近取得所需内容,提高了用户访问网站的响应速度。常常用于发布静态内容,如图片、脚本、样式表。
添加Expires头
如果我们在组件的响应头使用一个长久的Expires
头,那么这些组件就可以被缓存,这会在后续的页面浏览中避免不必要的HTTP请求。
Expires头
Expires
头明确的告诉浏览器当再次请求某个组件时,是否可以使用组件的缓存副本,来减少HTTP请求。

可以看出添加了还没有过期的Expires
头,响应状态是200 OK (from cache)
,而且实际上也没有进行HTTP请求。
长久的Expires
头应该包含任何不经常变化的组件,包括脚本、样式表和图片组件。但是HTML文档不应该使用长久的Expires
头,因为它包含动态内容,这些内容在每次用户请求时都将被更新。
如果使用了Expires
头,当页面内容改变时就必须改变内容的文件名。依Yahoo!来说我们经常使用这样的步骤:在内容的文件名中加上版本号,如yahoo_2.0.6.js
。
Max-Age
HTTP1.1引入了Cache-Control
头来克服Expires
头的限制。因为Expires
头使用一个特定的时间,它要求服务器和客户端的时钟严格同步。
换一种方式,Cache-Control
使用max-age
指令指定组件被缓存多久。它以秒为单位定义了一个相对时间,如果从组件被请求开始过去的秒数少于max-age
,浏览器就使用缓存的版本,这就避免了额外的HTTP请求。
需要注意的是,如果在响应中同时指定这两个响应头——Expires
和Cache-Control max-age
,max-age
指令将重写Expires
头。
Last-Modified
实际上,如果一个组件没有长久的Expires
头,它仍然会存储在浏览器的缓存中。在后续请求中,浏览器会检查缓存。为了提高效率,浏览器会向服务器发送一个条件GET请求
条件GET请求是基于响应Last-Modified
头和请求If-Modified-Since
头来实现的。浏览器可以从响应Last-Modified
头知道组件的最后修改时间,当浏览器再次对该组件发起请求时,它会使用If-Modified-Since
头将最后修改时间发送给服务器。
如果组件自生成日期以来没有改变过,服务器会返回一个304 Not Modified
状态码告诉浏览器可以使用其缓存的组件,并不再发送响应体,从而得到一个更小且更快的响应。
压缩组件
上面介绍的几点主要是从减少HTTP请求的思路来提出的解决方案。除此之外,我们还可以通过减小HTTP响应的大小来减少响应的时间。
gzip编码
使用gzip编码来压缩HTTP响应包,并由此减少网络响应时间。这是减少页面大小的最简单的技术,但影响是最大的。
Web客户端可以通过HTTP请求中的Accept-Encoding
头来表示对压缩的支持:
accept-encoding: gzip, deflate, sdch, br |
如果Web服务器看到请求中有这个头,就会使用客户端列出来的方法中的一种来压缩响应。Web服务器通过响应中的Content-Encoding
头来通知Web客户端。
content-encoding: gzip |
压缩通常能将响应的数据量减少将近70%。
压缩什么
压缩的成本有——服务器端会花费额外的CPU周期来完成压缩,客户端要对压缩文件进行解压缩。
通常,很多网站会压缩其HTML文档,同时压缩脚本和样式表也是非常值得的。图片不应该压缩,因为它们本来就已经被压缩了。试图对它们进行压缩只会浪费CPU资源,还有可能会增加文件大小。
将样式表放在顶部
将内联样式块和<link>
元素从页面<body>
移动到页面<head>
中,这样能提高渲染性能。
在HTML文件<body>
中指定外部样式表和内联样式块可能对浏览器的渲染性能产生不利影响。浏览器会阻塞页面的逐步呈现,直到所有外部的样式表都已被下载。内联样式块可能会导致reflow和页面跳动。因此,把外部样式表和内联样式块放在页面的<head>
中是很重要的。通过确保样式表首先被下载和解析,可以让浏览器逐步呈现页面。
HTML规范规定,始终把使用<link>
标签的外部样式表放在<head>
里。不要使用@import
。还要确保您指定的样式有正确的顺序。
将脚本放在底部
并行下载
HTTP1.1规范建议浏览器从每个主机名并行地下载两个组件。如果一个Web页面平均地将其组件分别放在两个主机名下,整体的响应时间可以减少大约一半。
脚本阻塞下载
浏览器在解析一个HTML文档时,会根据标签的先后顺序(从上到下),依次进行解析。在解析到<script>
标签后,会从服务器上下载脚本。但是在下载脚本时并行下载实际上是被禁用的——即使使用了不同的主机名。
其中一个原因是,脚本可能使用document.write
来修改页面内容,因此浏览器会等待,以确保页面能够恰当地布局。
另外一个原因是为了保证脚本能够按照正确的顺序执行。如果并行下载多个脚本,那就无法保证响应式按照特定顺序到达浏览器的。如果这些脚本之间存在依赖关系,那就可能会导致JavaScript错误。
这就意味着,使用脚本时,对于所有位于脚本以下的内容,逐步呈现都被阻塞了。将脚本放在页面越靠下的地方,就有越多的内容能够逐步地呈现。
正确地放置脚本
放置脚本的最好地方是页面的底部。这不会阻止页面内容的呈现,而且页面中的可视组件可以尽早下载。
不过在很多情况下,很难将脚本移到底部。例如,如果脚本使用document.write
向页面中插入了内容,就不能将其移动到页面中靠后的位置。此外还会有作用域问题。
经常出现的另一种建议是使用延迟脚本。DEFER
属性表明脚本不包含document.write
,浏览器得到这一线索就可继续进行呈现。
<script src="..." defer></script> |
不过,如果脚本可以被延迟,那么它就可以移到页面的底部。这是加速Web页面的最佳方式。
避免CSS表达式
CSS表达式已经被时代淘汰了,在这就不介绍了。避免在CSS中使用表达式。