这是关于《高性能网站建设指南》的读书笔记。
黄金法则
只有10%-20%的最终用户响应时间花在了下载HTML文档上,其余80%-90%的时间花在下载页面中所有的组件上。
规则一:减少HTTP请求数
1. CSS Sprites
将多个图片定位到整合成为一个图片,然后在用CSS来定位显示。同时还提到了图片热点(map标签)和data: URL模式。(实际生产中,应该极少会用到后边两种)
2. 合并脚本和样式表
顾名思义,就是将外联的所有脚本和样式表都整合成一个脚本和样式表。但是,需要注意的是,对于模块化的代码,将所有的JavaScript和CSS分别合并成为一个文件在开发环境中很难完成。例如,一个页面可能需要script1、script2和script3,而另一个文件可能需要script1、script3和script5。解决的方法是遵守编译型语言的模式,保持JavaScript模块化,而在生成过程中从一组特定的模块中生成一个目标文件。
伴随的代价是代码的复用率降低了(书中并没有提到),比如第一个页面将script1、script2和script3组合成为一个js文件,第二个页面将script1、script3和script5组合成为一个文件,显然代码script1和script3都被重新加载了一边。当然,作为文本的js文件,尤其是在压缩之后,一般情况下,重复加载的模块所带来的文件大小的增加应该是可以忽略的。
规则二:使用内容发布网络
你的用户肯能遍布世界的各个角落,即使同一台服务器,不同用户的页面响应时间也是不一样的,有些情况下会明显的影响到用户体验。一个解决方法是,采取分布式的构架重新设计你的web程序,像谷歌在全球的不同地域部署服务器一样。但是,这样巨大的开销和任务。另一个简单的解决方法就是,将组件web服务器从应用服务器中分离开来,托管于一些内容发布网络。
缺点也是显而易见,你的页面响应时间会受到托管服务器的影响,如果托管服务器的性能下降,你的网站性能也会下降。并且你也无法直接控制服务器组件,例如修改HTTP响应头必须通过服务提供商来完成,而不是你的团队。
规则三:添加Expires头
web服务器通过Expires头来告诉web客户端它可以使用一个组件的当前副本,直到指定的时间为止。如下:
Expires Thu, 15 Apr 2010 20:00:00 GTM
它告诉浏览器可以使用改组件的副本一直持续到2010年4月15号。
Expires 的缺点是过期的日期需要经常检查,一旦这一天到来以后,需要在服务器中配置一个新的日期。不过,在HTTP1.1中提供了另外的字段来完成同样的功能。在HTTP1.1中,Cache-Control使用max-age来指定组件被缓存多久,它以秒为单位。同样避免了额外的HTTP请求。例如:
Cache-Control: max-age=315360000
一个最佳的实践是,长久的Expires应该包含任何不经常变化的组件,包括脚本、样式表和Flash组件。
同时,我们也应该注意到,当我们配置组件更新时,在直到过期日期之间,浏览器都不会检查任何更新。为了确保用户能够获取最新的版本,需要在所有的HTML页面中修改组件的文件名。
规则四:压缩组件
对样式和样式表等组件进行压缩将会减少传输时间。压缩组件的一种方式是,删除文档之间的空格和注释,甚至缩短JS文件中的变量名等,现在有很多工具都可以做到这一点。另一种效果显著的方法是压缩传输的文件,从HTTP1.1开始,客户端可以通过Accept-Encoding头来标识对压缩的支持:
Accept-Encoding: gzip, deflate
web服务器通过Content-Encoding头来通知客户端采用了何种压缩:
Content-Encoding: gzip
其中,gzip是目前最有效和最流行的压缩方式。
规则五:将样式表放在顶部
通常组件的加载是按照文档中的出现的顺序来加载的,所以将不需要立刻使用的样式(比如用户点击弹出对话框的样式)放在文档的末尾来加载,就可以获得一个较好的体验效果。但是,这个结论是错误的。
在IE8及更早的IE浏览器中,页面的逐步呈现会在样式表下载完成前被阻止,因为在样式表加载完成之前就构建呈现树是没有必要的。这样做带来的结果是,HTML在加载时不会提供任何视觉反馈,一直等到样式加载完成后,然后才呈现出整个页面,也就是所说的“白屏”,会然用户感觉到“缓慢”。
在IE9和其他浏览器已经没有“白屏”问题,即使样式放在底部,页面也可以逐步呈现,但是样式加载和解析之后,呈现的文字和图片就需要用新的样式进行重绘,这同样造成了闪烁,依旧是一种不友好的用户体验。
另外,W3C标准明确说明应该把样式放在HTML文档的头部。
规则六:将脚本放在尾部
脚本在加载时会阻塞所有的并行下载,因此所有位于脚本以下的内容的逐步呈现都会被阻止,因此需要将脚本放在文档的尾部。
脚本会阻塞并行下载原因有二。其一,脚本可能使用了document.write来修改页面的内容,因此浏览器不得不等待,以确保页面的正确布局。其二,为了保证脚本能够按照正确的顺序执行。如果同时并行下载多个脚本,就无法保证响应是按照特定的顺序到浏览器的。如果它们之间存在着依赖关系,那么就可能导致JavaScript错误。
另外一种建议就是使用延迟(Defferred)脚本。Defer 属性表明脚本可以延迟加载,因此浏览器可以继续进行呈现。不过,在Firefox中,即便是延迟脚本也会阻塞呈现和并行下载。
规则七:避免使用CSS表达式
IE浏览器支持CSS表达式,而其他浏览器只会简单的忽略。CSS表单式的样式如下:
with: expreesion(document.body.clientWidth < 600 ? "600px" : "auto");
expreesion接受一个JavaScript表达式。expression真正的问题在于表达式可能会被极度频繁的求职,例如上边的例子,求值不仅发生在浏览器窗口大小变化时,而每一次鼠标的移动都会造成表达式求值计算,如果一个文本输入框获得焦点,那么甚至会因为反复求值造成浏览器奔溃。
规则八:使用外部JavaScript和CSS
纯粹而言,内联样式更快一些,因为不仅合并后文件大小更小一些,更重要的是有效减少了HTTP请求。但是考虑到JavaScript文件和CSS文件会被缓存,这时外部JS和CSS文件就会带来性能收益。
规则九: 减少DNS查询
Internet是通过IP地址来查找服务器的,这意味着浏览器中输入URL在发送请求前会有一次域名的解析过程。
浏览器和操作系统各自都有自己的DNS缓存,如果请求的DNS存在在缓存中,那么便不需要向上级DNS解析器发起请求。而且每一个DNS记录都有一个缓存时间(Time-to0live, TTL)值。
减少页面中主机名的数量会减少DNS查找,但带来的另一个潜在的问题是,同时会减[图片]少了页面中并行下载的数量。书中给出的建议是,如果页面中有大量的组件,那么将这些组件放到至少2个,但不要超过4个主机名下。这是在减少DNS查找和允许高度并行下载之间做出很好的权衡。
规则十:精简JavaScript
精简是从代码中移除不必[图片]要的字符以减小其大小,而混淆则在精简的基础上,还修改部分代码,一般而言,函数和变量名会被转换成为更短的字符。
混淆的目的是增加对代码进行反向工程的难度,对于精简而言,优点是可以进一步减小代码。其缺点是,混淆过程本身可能引入错误,维护和调试也会变得更加困难。
因此,采取混淆还是精简,最终的决定需要考虑混淆能够带来额外的代码大小的减少量。一般情况下,建议采用精简而不是混淆。规则四提到的压缩能够极大的减小传输代码的大小,大约70%,所以压缩会被精简更加有效。在采用了压缩的情况下,精简和混淆之间的区别会进一步减小,几乎一样。
规则十一:避免重定向
重定向是用于将用户从一个URL重新路由到另一个URL。重定向会延迟整个HTML文档的传输。下面介绍几种常见的重定向以及解决方案。
- 缺少结尾的斜线
在URL必须出现斜线时没有出现就可能导致重定向,例如,访问http://astrology.yahoo.com/astrology 时,就会产生一个301响应,其中包好了一个到 http://astrology.yahoo.com/astrology/ 的重定向。这是一种最为浪费、发生的也很贫乏的重定向。
解决的方法,一般通过配置web服务器就能解决这个问题。
- 连接网站
这种情况一般发生在网站被重写导致新的URL和旧的URL不一样,另外也包括将一个网站的不同部分连接起来等。重定向让连接两个网站很简单,而且只需要很少的额外代码。
解决的方法也较多,比如,如果后端位于同一台服务器上,则它们的代码可能自己均能够连接,如果域名变了,则可以使用一个CNAME让两个主机名指向相同的服务器。
- 跟踪内部流量
重定向经常用于跟踪内部的流量。解决方案比较复杂,可以参考书中。
规则十二:移除重复的脚本
导致一个脚本的重复有连个重要的因素——团队的大小和脚本的数量。而重复脚本损伤性能的方式有两种——不必要的HTTP请求和执行JavaScript所浪费的时间。
规则十三:配置ETag
实体标签(Entity Tag)是 web 服务器和浏览器用于确认缓存组件的有效性的一种机制。例如,请求如下:
GET /i/yahoo.gif HTTP 1.1
Host: us.yimg.com
服务器响应如下:
HTTP 1.1 200 OK
Last-Modified: Tue, 12 Dec 2006 03:03:59 GMT
ETag: "10c24bc-4ab-457e1c1f"
Content-Length: 1195
Etag 的加入为验证组件提供了比最新修改日期更为灵活的机制。例如,如果实体依据 User-Agent 或 Accept-Language 头而改变,实体的状态可以反映在 Etag 中。
此后,如果浏览器必须验证一个组件,他会使用 If-None-Match 头将 ETag 传回原始服务器。
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-45e1c1f"
如果 ETag 匹配的,就会返回 304 状态码。
HTTP 1.1 304 Not Modified
ETage 的问题在于,通常使用组件的某些特性来构造它,这些属性对于特定的、寄宿了网站的服务器来说是唯一的。当浏览器从一台服务器上获取了原始组件,之后,又向另一台不同的服务器发起条件 GET 请求时, ETag 是不会匹配的——而对于使用服务器集群来处理请求的网站来说,这是很常见的一种情况。依据 HTTP1.1 规范,如果请求中同时出现了 If-Modified-Since 和 If-None-Match 这两个头,除非请求中的条件头字段全部一致,否则服务器禁止返回 304。
解决的方法是配置或者移除ETag。通过配置 web 服务器或者通过后端应用程序直接控制,使得访问不同服务器上相同的组件时,返回的 ETag 是相同的。当然更加直接的方法是,不使用 ETag。
规则十四:使 Ajax 可缓存
确保 Ajax 请求遵循性能指导,尤其应具有长久的 Expires 头。
后记
本片文章只是简单的笔记,原书中提供了大量的实例从数据上去论证这些规则,如果需要有深刻的了解,应该去阅读原书。