性能优化规则总结(初级)

前言

书中有提到一个性能黄金法则

只有 10%~20% 的最终用户响应时间花在了下载 HTML 文档上。其余的 80%~90% 时间花在了下载页面中的其他组件上。

首先看看十大网站花在下载 HTML 文档上的时间百分比:

网站名称 无缓存 完整缓存
AOL 6% 14%
Amazon 18% 14%
CNN 19% 8%
eBay 2% 8%
Google 14% 6%
MSN 3% 5%
MySpace 4% 14%
Wikipedia 20% 12%
Yahoo! 5% 12%
YouTube 3% 5%

所有这些网站在获取 HTML 文档时,花费的时间都不到总响应时间的 20%。

在进行性能优化时,关键是剖析当前的性能,找到在哪里能够获得最大的改进。很明显,在这种情况下我们应该关注前端性能。

关注前端可以很好地提高整体性能。即使我们可以将后端响应时间缩短一半,整体的响应时间只能减少 5%~10%。而如果关注前端性能,同样是将其响应时间减少一半,则整体响应时间可以减少 40%~45%。并且,改进前端通常只需要较少的时间和资源。减少后端延迟会带来很大的改动,例如重新设计应用程序的架构和代码、查找和优化临界代码路径、添加或改动硬件、对数据库进行分布化等。这些改动需要花费数周或数月。而前端性能改进只需要一些最佳实践,例如修改 Web 服务器配置文件、将脚本和样式表放在页面中的特定位置、合并图片、脚本和样式表。这些改动只需要几个小时或几天,比进行后端改进要少花很多时间。

接下来总结下书中的 14 条性能优化的规则,仅作参考。

规则1——减少 HTTP 请求

80%~90% 时间花在为 HTML 文档所引用的所有组件(图片、脚本、样式表、Flash 等)进行的 HTTP 请求上。因此,改善响应时间的最简单途径就是减少组件的数量,并由此减少 HTTP 请求的数量(每次TCP三次握手的过程耗时大约几十毫秒)。

这些技术包括 CSS Sprites、内联图片和脚本、样式表的合并。在实际页面上运用这些技术响应时间可以减少到50%左右。

CSS Sprites

中文名称:雪碧图、精灵图。这里就不贴图举例了。

优点:

缺点:

内联图片(base64编码)

将文件进行 base64 编码再通过使用data:URL的模式可以在 Web 页面中包含图片但无需任何额外的 HTTP 请求。

由于 base64 代码是内联在页面中的,如果直接书写在 HTML 页面上的话,在跨越不同页面时不会被缓存。因此我们不要去内联网页的 Logo 或者是其他一些多页面共用的图片,因为重复编码会导致页面变大。在这种情况下,聪明的做法是使用 CSS 内联 base64 图片并将其作为背景 。

data:URL不仅可以用于编码图片,还可以用在任何需要指定 URL 的地方,例如 SCRIPT 和 A 标签。

优点:

缺点:

合并脚本和样式表

优点:

缺点:

规则2——使用 CDN(内容发布网络)

如果应用程序 Web 服务器离用户更近,则一个 HTTP 请求的响应时间将缩短。如果组件 Web 服务器离用户更近,则多个 HTTP 请求的响应时间将缩短。

CDN 是一组分布在多个不同地理位置的 Web 服务器,用于更加有效地向用户发布内容。在优化性能时,向特定用户发布内容的服务器的选择基于对网络可用度的测量。例如,CDN 可能选择网络阶跃数最小的服务器,或者具有最短响应时间的服务器。

除了缩短响应时间之外,CDN 还可以带来其他优势。它们的服务包括备份、扩展存储能力和进行缓存。CDN 还有助于缓和 Web 流量峰值压力。

CDN 用于发布静态内容,如图片、脚本、样式表和 Flash。

优点:

规则3——添加 Expires 头

如果说前面两个规则针对的都是首次访问优化,那么这个优化规则针对的就是用户的二次访问。今天的 Web 页面都包含了大量的组件,并且数量在不断增长。页面的初访者会进行很多 HTTP 请求,但通过使用一个长久的 Expires 头,可以使这些组件被缓存。这会在后续的页面浏览中避免不必要的 HTTP 请求。长久的 Expires 头最常用于图片,但应该将其用在所有组件上,包括脚本、样式表和 Flash。

Web 服务器使用 Expires 头来告诉 Web 客户端它可以使用一个组件的当前副本,直到指定的时间为止。HTTP 规范中简要地称其为“在这一日期/时间之后,响应将被认为是无效的”。它在 HTTP 响应中发送。格式如下:

Expires: Mon, 15 Apr 2024 20:00:00 GMT

这是一个有效期非常长久的 Expires 头,它告诉浏览器该响应的有效性持续到 2024 年 4 月 15 日为止。如果为页面中的一个图片返回了这个头,浏览器在后续的页面浏览中会使用缓存的图片,将 HTTP 请求的数量减少一个。

除了 Expires 头之外,还有另一种选择:HTTP 1.1 引入的Cache-Control

Cache-Control: max-age=315360000

Expires 头使用一个特定的时间,它需要要求服务器和客户端的时钟是同步的,因此假使两者时钟不同步,就可能出现意外情况。

换一种方式,Cache-Control 使用 max-age 指令指定组件被缓存多久。它以秒为单位,如果从组件被请求开始过去的秒数少于 max-age,浏览器就使用缓存的版本,使用带有 max-age 的 Cache-Control 可以消除 Expires 的限制,但对于不支持 HTTP 1.1 的浏览器,你可以提供 Expires 头。

同时指定这两个响应头——Expires 和 Cache-Control。如果两者同时出现,HTTP 规范规定 max-age 指令将重写 Expires 头。

如果是一些不需要经常更新的资源,应该主动为其添加 Expires 或 Cache-Control 头。

规则4——使用 gzip 压缩组件

这个规则通过减少 HTTP 响应的大小来减少响应时间。如果 HTTP 请求产生的响应包很小,传输时间就会减少。这里主要介绍使用 gzip 编码来压缩 HTTP 响应包,并由此减少网络响应时间。这时减小页面大小最简单的技术,但影响最大。还有很多方式可以减小 HTML 文档的页面大小,但他们需要更多的工作。

从 HTTP 1.1 开始,Web 客户端可以通过 HTTP 请求中的 Accept-Encoding 头来表示对压缩的支持。

Accept-Encoding: gzip, deflate

如果 Web 服务器看到请求中有这个头,就会使用客户端列出来的方法中的一种来压缩响应,Web 服务器通过响应中的 Content-Encoding 头来通知 Web 客户端。

Content-Encoding: gzip

gzip 是目前最流行和最有效的压缩方法,压缩的内容包括各种文本响应,例如脚本、样式表和 JSON 文件。图片和 PDF 不应该压缩,因为它们本来就已经被压缩了。试图对它们进行压缩只会浪费 CPU 资源,还哟扑可能会增加文件大小。

压缩的成本有——服务器端会花费额外的 CPU 周期来完成压缩,客户端要对压缩文件进行解压。要检测收益是否大于开销,需要考虑响应的大小、连接的带宽和客户端与服务器之间的链路距离。

压缩通常能将响应的数据量减少将近 70%。下面列出脚本和样式表的 gzip 压缩示例。

文件类型 未压缩大小 gzip 大小 gzip 节省
脚本 3277 Kb 1076 Kb 67%
脚本 39713 Kb 14488 Kb 64%
样式表 968 Kb 426 Kb 56%
样式表 14122 Kb 3748 Kb 73%

规则5——将样式表放在顶部

将样式表放在前面,会延迟页面中其他组件的加载,为什么反而会改善页面加载时间呢?

当浏览器逐步地加载页面时,页头、导航栏、顶端 Logo 等,所有这些都会为正在等待页面的用户提供视觉反馈。这改善了整体用户体验。

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

白屏现象源自浏览器的行为。尽管浏览器已经得到了所需组件,它依然要等到样式表下载完毕之后再呈现它们,样式表在页面中的位置不会影响下载时间,但是会影响页面的呈现。

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

有些浏览器在白屏和 FOUC 之间选择了 FOUC,例如 Firefox。当浏览器的行为不同时,我们前端工程师能做的,就是规范化编码风格。

我们应该始终将样式表放在文档的 HEAD 中。

规则6——将脚本放在底部

将脚本放在底部,这样页面既可以逐步呈现,也可以提高下载的并行度。

如果一个脚本下载需要 10 秒钟,那么页面的下半部分要花大约 10 秒才能显示出来,出现这一现象是因为脚本会阻塞并行下载(不同浏览器对此有不同的优化,WebKit 会另开线程进行下载)。

使用脚本时,对于所有位于脚本以下的内容,逐步呈现都被阻塞了。将脚本放在页面越靠下的地方,意味着越多的内容能够逐步地呈现。

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

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

至此,将脚本放在顶部对 Web 页面的影响就清楚了:

如果将脚本放在页面顶部,正如通常的情况那样,页面中的所有东西都位于脚本之后,整个页面的呈现和下载都会被阻塞,直到脚本加载完毕,因此会出现规则 5 中介绍到的白屏现象。逐步呈现对于良好的用户体验来说是非常重要的,但缓慢的脚本下载延迟了用户期待的反馈。此外,由于并行下载数的减少,不管图片显示有多块,都要被延迟。

解决方式有三个:

规则7——避免 CSS 表达式

CSS 表达式只有 IE 浏览器支持,如果CSS 表达式没有被谨慎的书写,可能会导致非常多次的调用。

p {
    width: expression( setCntr(), document.body.clientWidth<600 ? "600px" : "auto" );
    min-width: 600px;
    border: 1px solid;
}

setCntr()函数增加一个全局变量的值并将这个值写到页面中的一个文本框里。假使页面有 10 个段落,加载页面会执行 CSS 表达式 10 次。这之后,对于各种事件,如改变大小、滚动和鼠标拖拽,该 CSS 表达式都会被求值 10 次。在页面上来回拖拽鼠标可以很轻易地产生 10000 次以上的求值。在这个例子里的 CSS 的危险显而易见。最郁闷的是,在 IE 中点击这个页面的文本框之后,你就不得不终止这个进程了。

这里对 CSS 表达式不作探讨,但可以明确的是——在灭有深入了解底层影响的情况下使用 CSS 表达式是很危险的。

规则8——使用外部 JavaScript 和 CSS

纯粹而言,内联会更快一些,因为不用多出一个 HTTP 请求。

但这个规则很好理解,目的就是为了利用缓存,重用重复的 JavaScript 和 CSS 文件。

规则9——减少 DNS 查找

通常,浏览器进行一次 DNS 查找要花费 20~120 毫秒。在 DNS 查找完成之前,浏览器不能从主机名那里下载到任何东西。响应时间依赖于 DNS 解析器、它所承担的请求压力、你与它之间的距离和你的带宽速度。

好在 DNS 查找可以被缓存起来以提高性能。这种缓存可以发生在浏览器上,或者是本地服务器上。

在用户请求一个主机名之后,DNS 信息会留在操作系统的 DNS 缓存中,之后对于该主机名的请求将无需进行过多的 DNS 查找,至少短时间内不需要。

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

尽管操作系统缓存会考虑 TTL 值,但浏览器通常忽略该值,并设置它自己的时间限制。HTTP 协议中的 Keep-Alive 特性可以同时覆盖 TTL 和浏览器的时间限制。换句话说,只要浏览器和 Web 服务器愉快地通信着,并保持着 TCP 连接打开的状态,就没有理由进行 DNS 查询。

浏览器对缓存的 DNS 记录的数量也有限制,而不管缓存记录的时间。如果用户在短时间内访问了很多具有不同域名的网站,较早的 DNS 记录将被丢弃,必须重新查找该域名,

不过,要记得即便浏览器丢弃了 DNS 记录,操作系统可能依然保留着该记录,这能扭转一下局面,因为无需通过网络发送查询,从而避免了明显的延迟。

我们应该避免使用过短的 TTL 值,它的建议值是 1 天。

但是一些大型网站,他们的 TTL 值都是比较短的,1~60分钟不等。因为这些拥有巨大数量用户的顶级网站都在努力做到当服务器、虚拟 IP 地址或联合定位掉线时提供快速故障转移。

所以,我们应该尽可能减少 DNS 查找:

规则10——精简 JavaScript

上线时压缩(ugily) JavaScript 文件,减少响应大小。

下面列一下使用 JSMin 和 Dojo Compressor 两种压缩工具得到的大小减小量:

网站 原始大小 JSMin 节省 Dojo Compressor 节省
http://www.amazon.com/ 204KB 31KB(15%) 48KB(24%)
http://www.aol.com/ 44KB 4KB(10%) 4KB(24%)
http://www.cnn.com/ 98KB 19KB(20%) 24KB(25%)
http://www.myspace.com/ 88KB 23KB(27%) 24KB(28%)
http://www.wikipedia.com/ 42KB 14KB(34%) 16KB(38%)
http://www.youtube.com/ 34KB 8KB(22%) 10KB(29%)
平均 85KB 17KB(21%) 21KB(25%)

结合之前的规则 4,gzip 压缩产生的影响最大,但精简能够进一步减小文件大小。随着 JavaScript 的使用量大小不断增长,精简 JavaScript 代码能够得到更多的节省。

规则11——避免重定向

重定向(Redirect)用于将用户从一个 URL 重新路由到另一个 URL。重定向有很多种——301 和 302 是最常用的两种。通常针对 HTML 文档进行重定向,但也有可能用在请求页面中的组件(图片、脚本等)时。实现重定向可能有很多不同的原因,包括网站重新设计、跟踪流量、记录广告点击和建立易于记忆的 URL。主要要记得的是,重定向会使你的页面变慢。关于重定向是如何损伤页面性能的,本文不作介绍。

规则12——删除重复脚本

重复的脚本在 IE 中会产生不必要的 HTTP 请求,并且对脚本进行多次运行也会浪费时间。对 JavaScript 进行多余的执行在 IE 和 Firefox 中都存在,与脚本被缓存与否无关。

规则13——配置 Etag

对于配置缓存,Etag 提供了另外一种方式,用于检测浏览器缓存中的组件与原始服务器上的组件是否匹配。Etag 在 HTTP 1.1 中引入。Etag 是唯一标识了一个组件的一个特定版本的字符串。唯一个格式约束是该字符串必须用引号引起来。

// 服务器端 response
HTTP 1.1 200 OK
Last-Modified: Tue, 12 Dec 2006 03:03:59 GMT
Etag: "10c24bc-4ab-457e1c1f"
Content-Length: 1195

Etag 的加入为验证实体提供了比最新修改日期更为灵活的机制,直接验证版本是否相同。

此后,如果浏览器必须验证一个组件,它会使用 If-None-Match 头将 Etag 传回原始服务器。如果 Etag 是匹配的,就会返回 304 状态码,使响应减少了 1195 字节。

// 客户端 request
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"

// 服务器端 response
HTTP 1.1 304 Not Modified

Etag 的格式是inode-size-timestamp。文件系统使用inode来存储诸如文件类型、所有者、组和访问模式等信息。尽管在多台服务器上一个给定的文件可能位于相同的目录、具有相同的文件大小、权限、时间戳等,从一台服务器到另一台服务器,其inode仍然是不同的。

又因为If-None-MatchIf-Modified-Since具有更高的优先级。如果请求中同时出现了这两个头,则只会比较If-None-Match是否一致,而不同服务器上相同文件的inode都是不同的,所以使用 Etag 以后就可能导致一个情况:当有多台服务器负载时,用户每次请求,返回的If-None-Match都不相同,导致即使缓存的是相同的文件,仍需要每次重新下载,造成浪费。

解决办法:

规则14——使 Ajax 可缓存

这一点的大概意思是,GET 方法的请求结果要幂等,并采用 Expires 头,让采用的 GET 方法的 Ajax 能够缓存结果。