《高性能网站建设指南》

14条性能规则

  • 规则1:减少HTTP请求,介绍为什么额外的HTTP请求会对性能产生巨大的影响,并介绍了减少HTTP请求的 方法,包括图片地图,CSS Spries, 使用data:模式的URL内联图片,以及合并脚本和样式表
  • 规则2:使用内容发布网络(CDN),强调使用内容分发网络的优势
  • 规则3:添加Expires头,研究一个简单的HTTP头是如何通过使用浏览器缓存来戏剧性地改善Web页面性能
  • 规则4:压缩组件,解析压缩是如何工作的,以及如何为Web服务器启用压缩,并讨论了现今存在的一些兼容性问题
  • 规则5:将样式表放在顶部,展示脚本是如何影响呈现的
  • 规则6:将脚本放在底部,展示脚本是如何影响呈现的,以及脚本是如何下载到浏览器中的
  • 规则7:避免CSS表达式,讨论CSS表达式的使用和度量其影响的重要性
  • 规则8:使用外部javascript和css,介绍如何权衡内联javascript和css,还是将他们放在外部文件中
  • 规则9:减少DNS查找,强调解析域名是的频繁查找所产生的影响
  • 规则10:精简javascript,量化从javascript中移除空白字符所带来的收益
  • 规则11:避免重定向,使用重定向提出了警示,并给出了可替代的方法
  • 规则12:移除重复脚本,展示如果一个页面中包含了两处相同的脚本会发生什么情况
  • 规则13:配置ETag,介绍ETag是如何工作的,以及为什么对于任何拥有多余一台Web服务器的网站来说,默认的实现都是不好的
  • 规则14: 使Ajax可缓存,强调在使用Ajax时牢记这些性能规则的重要性

前言

  • HTTP响应的状态码302,响应头中没有缓存信息,因此浏览器无法缓存该响应
  • 在请求脚本时不会发生并行请求
  • 在进行优化前,关键是剖析当前的性能,找到在哪里能够获得最大的改进
  • 性能黄金法则:只有10%20%的最终用户响应时间花在了下载HTML文档上。其余的80%90%时间花在了下载页面中的所有组件上

Ruel1:Make Fewer HTTP Requests – 减少HTTP请求

图片地图

图片地图(image map)允许你在一个图片上关联多个URL。目标URL的选择取决于用户单击图片上的哪个位置
例如:一个导航栏上有五福图片,对应五个分开的超链接,使用了五个分开的图片来实现。然而,如果使用一个图片地图则可以更有效率,五个HTTP请求被减少为只有一个的HTTP请求
图片地图有两种类型:

  • 服务端图片地图:将所有点击提交到同一个目标URL,并向其传递用户x,y坐标
  • 客户端图片地图:更加典型,将用户点击映射到一个操作上
    缺点: 在定义图片地图上的区域坐标时,如果采用手工方式则很难完成且容易出错,而且除了矩形之外几乎无法定义其他形状

CSS Sprites

合并图片,将多个图片合并到一个单独的图片中
图片地图的图片必须是连续的,而CSS Sprites则没有这个限制
实际上合并后的图片比分离的图片总和要小,这是因为它减低了图片自身的开销

内联图片

通过data: URL模式可以在Web页面中包含图片但无需额外的HTTP请求。
格式:

1
data:[<mediatype>][;base64],<data>

data: 都是用在内联图片,但它可以用在任何需要指定URL的地方,包括script和A标签
主要缺陷:不受IE的支持,存在数据大小上的限制
不要去内联公司Logi,因为编码过的Logo会导致页面变大。这种情况下,聪明的做法是使用CSS并将内联图片作为背景,将该CSS规则放在外部样式表中,这意味着数据可以缓存在样式表内部。

合并脚本和样式表

使用外部脚本和样式表对性能更有利,然而分开到多个小文件中,会降低性能,因为每个文件都会导致一个额外的HTTP请求,应将这些单独的文件合并到一个文件中。
不建议脚本和样式表合并在一起,但是多个脚本应该合并为一个脚本,多个样式表应合并为一个样式表

Rule2: Use a Content Delivery Netword – 使用内容分发网络

内容分发网络(CDN)是一组分布在不同地理位置的Web服务器,用于更加有效地向用户发布内容
除了缩短响应时间之外,CDN还可以带来其他优势:备份,扩展存储能力和进行缓存,有助于缓和Web流量峰值压力
缺点是你的响应时间可能会受到其他网站的影响,还有无法直接控制组件所带来的麻烦,如修改HTTP响应头必须通过服务提供商来完成
CDN用于发布静态内容,如图片,脚本,样式表和Flash

Rule3: Add an Expires Header – 添加Expires头

Expires 头

Web服务器使用Expires头来告诉Web客户端它使用一个组件的当前副本,直到制定的时间为止

Max-Age和mod_expires

HTTP1.1引入了Cache-Contorl头来克服Expires头的限制,因为Expires头使用一个特定的时间,它要求服务器和客户端的时钟严格同步。另外过期日期需要经常检查,并且一旦未来这一天到来了,还需要在服务器配置中提供一个新的日期
Cache-Control使用max-age指令制定组件被缓存多久,它以秒为单位定义了一个更新窗
使用带有max-age的Cache-Control可以消除Expires的限制,但对于不支持HTTP1.1的浏览器,你可能仍然希望提供Expires头,可以同时制定两个响应头:Expires和Cache-Control max-age。如果两者同时出现,HTTP规范规定max-age指令将重写Expires头
mod_expires apache模块使你在使用Expires头时能够像max-age那样以相对的方式设置日期

1
2
3
<FilesMatch "\.(gif|jpg|js|css)$">
ExpiresDefault "access plus 10 years"
</FilesMatch>

跨浏览器改善缓存的最佳方案就是使用ExpiresDefault设置的Expires头

空缓存 VS 完整缓存

不仅仅是图片

脚本,样式表,Flash组件,然而HTML文档不应该使用长久的Expires头,因为它包含动态内容
如果组件是因为经常变化而不被缓存,期望能看到Last-Modified日期

修订文件名

为了确保用户能获取组件的最新版本,需要在所有HTML页面中修改组件的文件名

最有效的解决方案是修改其所有链接,这样,全部的请求将从原始服务器下载最新的内容

将版本号嵌在组件的文件名中

Rule4: Gzip Components – 压缩组件

使用gzip编码来压缩HTTP响应包,减少网络响应时间

压缩是如何工作的

Web客户端通过HTTP请求中Accept-Encoding头来标识对压缩的支持

1
Accept-Encoding: gzip, deflate

如果Web服务器看到请求有这个头,就会使用客户端列出来的方法中的一种来压缩响应,通过响应中的Content-Encoding:gzip来通知Web客户端
gzip是目前最流行和最有效的压缩方法

压缩什么

HTML,脚本,样式表,XML/JSON在内的任何文本响应都应该压缩。图片和PDF不应该压缩因为它们本来就已经被压缩了,试图对它们进行压缩只会浪费CPU
压缩的成本:

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

节省

压缩通常可以将响应数据量减少将近70%

配置

mod_gzip(apache 1.3)/mod_deflate(apache 2.x)

代理缓存

当浏览器通过代理来发送请求时,情况变得复杂:

  • Client1(不支持压缩)通过代理(没有缓存)转发第一个请求到Web服务器,返回响应后,代理会缓存没有压缩的响应,而Client2(支持压缩)通过代理转发第二个请求时,代理直接返回了没有压缩的数据,失去了压缩的机会
  • Client1(支持压缩)通过代理(没有缓存)转发第一个请求到Web服务器,返回响应后,代理会缓存了压缩了的响应,而Client2(不支持压缩)通过代理转发第二个请求时,代理直接返回了压缩的数据,导致更严重的情况
    解决这一问题的方法是在Web服务器的响应中添加Vary头,告诉代理根据一个或多个请求头来改变缓存的响应。
    1
    Vary: Accept-Encoding

这使得代理缓存响应的多个版本

边缘情形

错误并不会经常发生,但它们是必须考虑的边缘情形
一种安全的方式是只为已经证实过支持压缩的浏览器提供压缩内容(浏览器白名单)

1
2
BrowserMath ^MSIE [6-9] gzip
BrowserMath ^Mozilla/[5-9] gzip

代理缓存和边缘情形将使情况变得更加复杂,不可能和代理共享浏览器白名单配置
最佳的做法是将User-Agent作为代理的另一种评判标准添加到Vary头中

1
Vary: Accept-Encoding, User-Agent

另外一种做法是使用Vary: * 或 Cache-Control: private头来禁用代理缓存,这会为所有浏览器禁用代理缓存,因此会增加你的带宽开销

Rule5 : Put Stylesheets at the top – 将样式表放在顶部

逐步呈现

可视化回馈的重要性:

进度指示器有三个主要优势,他们让用户知道系统没有崩溃,只是正在为他们解决问题;它们指出了用户大概还需要等待多久,以便用户能够在漫长的等待中做其他事情;最后,它们能给用户提供一些可以看的东西,使得等待不再是那么无聊。最后一点优势不可低估,这也是为什么推荐使用图形进度条而不是仅仅以数字形式显示预期的剩余时间

将样式表放在文档底部会导致浏览器中阻止内容逐步呈现,
规则5对加载页面所需实际时间没有太多影响,它影响更多的是浏览器对这些组件顺序的反应。实际上,用户感觉缓慢的页面反而是可视化组件加载得更快地页面

白屏

在IE中,将样式表放在文档底部会导致白屏问题的情形有以下几种:

  • 在新窗口中打开时
  • 重新加载时
  • 作为主页时

使用@import规则会导致组件下载时的无序性,应该使用LINK标签将样式表放在HEAD中

无样式内容的闪烁

如果样式表仍在加载,构建呈现树就是一种浪费,因为在所有样式表加载并解析完毕之前无需绘制任何东西。否则,在其准备好之前显示内容会遇到FOUC(无样式内容的闪烁,Flash of Unstyled Content)问题(取决于你的浏览器以及如何加载页面)
当页面逐步加载时,文字首先显示,然后是图片,最后,在样式表正确地下载并解析之后,已经呈现的问题文字和图片要用新的样式重绘,这就是“无样式内容的闪烁”,应该避免这个问题
白屏是对FOUC问题的弥补。浏览器可以延迟呈现,知道所有的样式表都下载完之后,这就导致了白屏。反之,浏览器可以逐步呈现,但要承担闪烁的风险。这里没有完美的选择

前端工程师应该做什么?

如何避免白屏和无样式内容的闪烁?

和A不一样,LINK只能出现在文档的HEAD中,但其出现次数是任意的

Rule 6:Put Scripts at the Bottom – 将脚本放在底部

脚本带来的问题

脚本阻塞了并行下载,将脚本放在页面越靠下的地方,意味着越多的内容能够逐步呈现。

并行下载

对响应时间影响最大的是页面中组件的数量。HTTP1.1规范建议浏览器从每个主机名并行下载两个组件
Yahoo的研究表明,使用两个主机名比使用1,4或者10个主机名能带来更好的性能

脚本阻塞下载

并行下载组件的优点很明显,但下载脚本时并行下载实际上是被禁用的
原因是:

  • 脚本可能使用document.write来修改页面内容,因此浏览器会等待,以确保页面能够恰当地布局
  • 为了保证脚本能够按照正确的顺序执行,如果并行下载多个脚本,就无法保证响应是按照特定顺序到达浏览器的(如果存在依赖关系,就会导致js错误)
    放置脚本的最好地方是页面的底部,这不会阻止页面内容的呈现,而且页面中的可视组件可以尽早下载

正确的放置

在很多情况下,很难将脚本移到底部,如脚本使用document.write向页面中插入了内容,就不能将其移动到页面中靠后的位置。此处还会有作用域问题。
另一种建议是使用延迟(defferred)脚本,DEFER属性表明脚本不包含document.write,浏览器得到这一线索就可以继续进行呈现,但是在firefox中延迟脚本也会阻塞呈现和并行下载
如果一个脚本可以延迟,那么它一定可以移到页面的地步,这就是加速Web页面的最佳方式

Rule 7: Avoid CSS Expressions – 避免CSS表达式

CSS表达式是动态设置CSS属性的一种强大(并且危险)的方式
对CSS表达式的频繁求值使其得以工作,但也导致CSS表达式的低下性能

CSS表达式不只在页面呈现和大小改变时求值,当页面滚动,甚至页面鼠标在页面上移过时都要求值。

围绕问题展开工作

创建一次性表达式和使用事件处理器取代CSS表达式

一次性表达式

如果CSS表达式必须被求值一次,那么可以在这一次执行中重写它自身

事件处理器

可以尝试使用事件处理器来为特定事件提供所期望的动态行为,但还是需要使用一次性表达式

Rule 8: Make JavaScript and CSS External – 使用外部javascript和css

内联VS外置

  • 纯粹而言,内联会比外置快,因为只需要更少的HTTP请求,但是考虑外部JavaScript和CSS有机会被浏览器缓存起来。
  • 当每个用户产生的页面查看越少,内联论据越强势,相反则是外置更加有利
  • 研究表明,具有完整缓存的页面查看数量占75%~85%,一般情况下,用户可能只有第一次访问携带的是空缓存,之后多次后续页面查看都是具有完整缓存。

组件重用

  • 如果每个页面都是用了相同的JavaScript和CSS,使用外部文件可以提高这些组件的重用率
  • 折中的做法是将页面划分成几种页面类型,然后为每种类型创建单独的脚本和样式表
  • 关键点在于与HTML文档请求数量相关的外部JavaScript和CSS组件被缓存的频率

主页

  • 使用内联方式反而更好的一个例子是主页(重用率很低的情况下)

两全其美

可以通过主页(内联JavaScript和CSS)加载完成后动态下载外部组件来实现(通过onload事件)
如果JavaScript和css被加载到页面中两次(先是内联,然后是外部的),要使其能够工作,必须处理双重定义

动态内联

主页服务器根据一个组件的cookie判断选择是内联还是外部文件

Rule9: Reduce DNS lookups – 减少DNS查找

DNS缓存和TTL

  • 操作系统的DNS缓存(windows上的DNS缓存由DNS client服务进行管理)
  • 浏览器的DNS缓存,DNS记录数量也有限制(重新启动浏览器会清空浏览器缓存,但不会清空DNS Client缓存)
  • TTL: DNS记录存活时间(time-to-live),这个值告诉客户端可以对该记录缓存多久,操作系统会考虑该值,但浏览器一般会忽略
  • HTTP中的Keep-Alive特性可以同时覆盖TTL和浏览器的时间限制
  • 通常使用Keep-Alive和较少的域名来减少DNS查找,建议将组件分别放到至少2个,但不要超过4个主机名下。

Rule10 : Minify JavaScript – 精简JavaScript

精简

精简是从代码中移除不必要的字符以减少其大小,进而改善加载时间的实践,在代码被精简之后,所有的注释以及不必要的空白字符(空格,换行,制表符)都被移除。这可以改善响应时间效率,因为需要下载的文件大小减少了。

混淆

混淆是可以应用在源代码上的另外一种优化方式,它会移除注释和空白,同时它还会改写代码,作为改写的一部分,函数和变量的名称将被转换为更短的字符串,这时的代码更加精炼,也更难阅读。通常这样做是为了增加对代码进行反向工程的难度,但对提高性能也有帮助。
存在三个主要的缺点:

  • 缺陷:由于混淆更加复杂,混淆过程本身很可能引入错误
  • 维护:由于混淆会变化JavaScript的符号,因此需要对任何不能改变的符号进行标记,防止混淆器修改他们
  • 调试:代码很难阅读,生产环境调试问题更加困难

节省

精简JavaScript最流行的工具是JSMin,还有Dojo Compressor(改名为ShrinkSafe)

精简CSS

精简CSS带来的节省通常要小于精简JavaScript,最大的潜在节省来自于优化CSS–合并相同的类,移除不适用的类等

Rule11 : Avoid Redirects – 避免重定向

重定向用于将用户从一个URL重新路由到另一个URL,最常用301和302

重定向类型

  • 300 Multiple Choices
  • 301 Moved Permancently
  • 302 Moved Temporarily
  • 303 See Other
  • 304 Not Modified(并不真的是重定向,用来响应请求,避免下载已经存在于浏览器缓存中的数据)
  • 305 Use Proxy
  • 306 不再使用
  • 307 Temporary Redirect

重定向是如何损伤性能的

重定向完毕并且HTML文档下载完毕之前,没有任何东西显示给用户

结尾缺少斜线

很多Web开发人员会把URL结尾不带斜线(/)的请求重定向到带斜线的中,这种重定向最为浪费,发生得也很频繁
结尾斜线的理由是为了因为它允许自动索引并且能够获得与当前目录相关的URL,但很多web页面并不依赖自动索引,而是依赖特定的URL和处理器

连接网站

将用户从旧的URL转移到新的URL的最简单的方式就是重定向,虽然降低了开发的复杂性,但是这也损害了用户体验
整合两个后端还有其他的选择:

  • Alias、mod_rewrite和DirectorySlash要求除了URL之外还要提交一个接口(处理器或文件名),但易于实现
  • 如果两个后端处于同一台服务器,代码很可能就可以自己连接,如旧的代码通过编译调用新的代码
  • 如果域名变了,可以使用CNAME,让两个主机名指向相同的服务器

跟踪流量

  • 使用301响应来跟踪内部跳转情况,另一种选择是使用日志跟踪流量去向,通过建立Referer日志来避免重定向
  • 对于跟踪出站流量Referer就不行了,但可以选择使用信标,一个HTTP请求(可以使用XMLHttpRequest来发送),其URL中包含有跟踪信息

美化URL

使用重定向的另一种动机是使URL更加美观并且易于记忆
与其让用户忍受额外的HTTP请求,最好还是使用Alias,mod_rewrite,DirectorySlash和直接链接代码来避免重定向

Rule12 : Remove Duplicate Scripts – 移除重复脚本

导致一个脚本重复有两个主要因素:

  • 团队大小
  • 脚本数量

重复脚本损伤性能的方式有两种:

  • 不必要的HTTP请求(IE中)
  • 执行JavaScript所浪费的时间

避免包含同一个脚本两次的方法:

  • 模板系统中实现一个脚本管理模块
  • 在php中创建一个称谓insertScript的函数来对重复插入脚本进行检测

Rule13 : Configure ETags – 移除/配置ETag

ETag

实体标签(entity tag, ETag)是Web服务器和浏览器用于确认缓存组件的有效性的一种机制
ETag在HTTP1.1中引入,ETag是唯一表示了一个组件的一个特定版本的字符串,唯一的格式约束是该字符串必须用引号引起来

Expires头

浏览器下载组件时,会将他们缓存起来,后续的页面查看中,会根据Expires头来判断组件是否过期
如果组件缓存过期了,浏览器在使用前必须首先检查它是否仍然有效,这称作一个条件GET请求,浏览器必须生产这个HTTP请求,执行有效性检查,如果缓存的组件是有效的,服务器不会返回整个组件,而是返回304状态吗

而服务器有两种方式检测缓存组件是否和原始服务器上的组件匹配:

  • 比较最新修改日期(Last-Modified,If-Modified-Since)
  • 比较实体标签(ETag,If-None-Match)

ETag带来的问题

ETag通常使用组件的某些属性来构造它,这些属性对于特定的,寄宿了网站的服务器来说是唯一的,当服务器集群的情况下,请求漂移后ETag是不会匹配的,对于拥有多台服务器的网站,ETag中嵌入的数据都会大大降低有效性验证的成功率

Apache中ETag格式是inode-size-timestamp,IIS中ETag格式是Filetimestamp:ChangeNumber

ETag还降低了代理缓存的效率,代理后面的用户缓存的ETag经常和代理缓存的ETag不匹配,这导致不必要的请求被发送到原始服务器,用户和代理之间不会出现304响应,而是会产生两个200响应(服务器到代理,代理到用户)。

ETag的默认格式还可能会引入安全性弱点

If-None-Match比If-Modified-Since具有更高的优先级,如果请求中同时出现了这两个头,则原始服务器禁止返回304,除非请求中的条件头字段全部一致。实际上如果没有If-None-Match头反而会更好一些

ETag用还是不用

即便你的组件具有长久的Expires头,一旦用户单击了Reload和Refresh按钮,依然会产生条件GET请求。ETag带来的问题是必须面对的

一种选择是对ETag进行配置,以利用其灵活的验证能力
如果你的组件必须通过最新修改日期之外的一些东西进行验证,则ETag是一种强大的方法
如果你无须自定义ETag,最好简单的将其移除。apache和iis都讲ETag视为一个性能问题,并建议修改ETage的内容

apache中移除ETag:

1
FileETag none

Rule14 : Make Ajax Cachedable – 使Ajax可缓存

Web2.0 DHTML和Ajax

Web2.0 包含很多概念,其中之一就是DHTML,Ajax也是DHTML种的一项关键技术

  • Web2.0的关键概念包括类似应用程序的用户界面和来自多个Web Services的聚合信息
  • DHTML:动态html允许页面加载完毕后,html页面的表现能够变化,这是用JavaScript和css与浏览器的文档对象模型(DOM)进行交互来实现
  • Ajax:异步JavaScript和xml,不是一个单独,需要许可证的技术,而是一组技术,包括JavaScript,css,dom和异步数据获取。目的是为了突破web本质的开始-停止交互方式。XMLHttpRequest是ajax的心中

异步与即时

ajax的一个明显的优点就是向用户提供了即时反馈,因为它异步从后端服务器请求信息,但并不代表用户不需要等待ajax返回响应,用户是否需要等待取决于如何使用ajax,其关键因素在于ajax其你去是被动的还是主动的

  • 被动请求:是为了将来使用而预先发起的
  • 主动请求:是基于用户当前的操作而发起的(即便主动的ajax请求是异步的,用户可能仍然需要等待响应,异步并没有暗示即时)

优化ajax请求

改善这些主动ajax请求的最重要的方式就是使响应可缓存,其他的13个规则中有一些也适用于ajax请求

  • 规则4: 压缩组件
  • 规则9:减少DNS查找
  • 规则10:精简JavaScript
  • 规则11:避免重定向
  • 规则13:移除/配置ETag

如果你可以为他缓存响应,就会看到缓存的和快速的用户体验之间的差距
响应的个性化和动态本质必须被反映到缓存中,可供采用的最好的方式就是使用查询字符串参数(类似将用户名放在查询字符串中)
当数据被认为是私有的时候,大多会使用Cache-Contorl:no-store,但HTTP规范警告说不要依赖这一机制来确保数据的隐私性,恶意的或危险的缓存会完全忽略Cache-Contorl:no-store头,更好的方法是使用SSL

确保ajax请求遵守性能指导,尤其应具有长久的Expires头

坚持原创技术分享,您的支持将鼓励我继续创作!.