前端性能的重要性
只有10%~20%的最终用户响应时间是花在从web服务器获取html文档并传送到浏览器的,如果希望有效减少页面的响应时间,就必须关注剩余80%~90%的最终用户体验。
Yahoo首页首次访问html文档只占响应时间的5%,用户需要花费其余95%的时间中的大部分来等待组件的下载。还有一小部分时间花在解析html、脚本和样式表上面。(打开浏览器的network可以直观的看到各部分所花的时间)。
当第二次加载同一个url时,html文档占了总响应时间的12%。很多组件无需下载,因为它们已经存在于浏览器的缓存中了。
为此,后续内容会从前端对减少用户最终响应时间给出精确的指导,并提出14条提升性能的规则,这些规则按常规的优先级顺序列出。对于特定的网站,规则的适用性可能会不同。但一般来说,只需要遵守这些最佳实践就能节省25%或更多的时间。
HTTP概述
HTTP是浏览器和服务器通过Internet进行相互通信的协议。Http1.1是今天比较常见的版本,但是一些浏览器和服务器还在使用Http1.0。Http是一种客户端服务器协议,由请求和响应构成。浏览器向一个特定的url发送http请求,url对应的宿主服务器发回http响应。和很多Internet服务一样,该协议使用简单的纯文本格式。请求的类型有GET、POST、HEAD、PUT、DELETE、OPTIONS和TRACE。我们主要关注最常见的GET请求。
GET请求包含一个url,然后是请求头。http响应包含状态码、响应头和响应体。
下面的例子展示了一个get请求:
压缩
浏览器可以使用Accept-Encoding头来声明它支持压缩。服务器使用Content-Encoding头确认响应已被压缩。
条件GET请求
如果浏览器在其缓存中保留了组件的一个副本,但并不确定它是否仍然有效,就会生成一个条件get请求。如果确认缓存的副本仍然有效,浏览器就可以使用缓存中的副本,这会得到更小的响应和更快的用户体验。
典型情况下,缓存副本的有效性源自其最后修改时间。基于响应中的Last-Modified头,浏览器可以知道组件最后的修改时间,它会使用If-Modified-Since头将最后修改时间发送给服务器。
如果组件自生成日期以来没有改变过,服务器会返回一个“304 Not Modified”状态码并不在发送响应体。在http1.1中,ETag和If-None-Match头是进行条件get请求的另外一种方式。后续会讲到。
Expires
条件get请求和304响应仍需要在客户端和服务器之间进行一次往返确认,Expires头通过明确指出浏览器是否可以使用组件的缓存副本来消除这个需要。
当浏览器看到响应中有一个Expires头时,它会和相应的过期时间组件一起保存到其缓存中。只要组件没有过期,就会使用缓存而不会发送任何人http请求。
Keep-Alive
持久连接的引入解决了多对一请求服务器导致的socket连接低效性的问题,它使浏览器可以在一个单独的连接上进行多个请求。浏览器和服务器使用Connection头来指出对Keep-Alive的支持。在服务器的响应中Connection头看起来是一样的。
浏览器或服务器通过发送一个Connection:close头来关闭连接。Connection:keep-alive并不是http1.1中必需的,但很多浏览器和服务器都包含它。
http1.1中定义的管道可以在一个单独的socket上发送多个请求而无需等待响应。管道的性能优于持久连接,但一些浏览器并不支持管道。在管道被广泛应用之前,Keep-Alive依然是浏览器和服务器使用http的socket连接最有效的方式。
规则1 减少HTTP请求
改善响应时间的最简单途径就是减少组件的数量,并由此减少http请求的数量。本章介绍的技术即可以减少http请求,又能避免在性能和设计之间的冲突。
图片地图
一个导航栏上有五幅图片,点击一个图片会将你带到与之相关的链接。这可以通过五个分开的超链接、使用五个分开的图片来实现。然而,如果使用一个图片地图则可以更有效率,因为五个http请求被减少为只有一个http请求。响应时间将会降低,因为减少了HTTP开销。
图片地图有两种类型。服务器端图片地图将所有点击提交到同一个目标URL,向其传递用户点击的x、y坐标。web应用程序将该x、y坐标映射为适当的操作。客户端图片地图更加典型,因为它可以将用户的点击映射到一个操作,而无需向后端应用程序发送请求。映射通过html的map标签实现。1
2
3
4
5
6
7
8<img usemap="#map1" border=0 src="/images/imagemap.gif?t=1196816255">
<map name="map1">
<area shape="rect" coords="0,0,31,31" href="home.html" title="Home">
<area shape="rect" coords="36,0,66,31" href="gifts.html" title="Gifts">
<area shape="rect" coords="71,0,101,31" href="cart.html" title="Cart">
<area shape="rect" coords="106,0,136,31" href="setting.html" title="Settings">
<area shape="rect" coords="141,0,171,31" href="help.html" title="Help">
</map>
使用图片地图也有缺点。在定义图片地图上的区域坐标时,如果采取手工的方式则很难完成且容易出错,而且除了矩形之外几乎无法定义其他形状。
CSS Sprites
和图片地图一样,CSS Sprites也可以合并图片,但更为灵活,任何支持背景图片的html元素都可以使用,如span或div。它和图片地图几乎一样快。图片地图中的图片必须是连续的,而css sprites则没有这个限制。
还有一个令人惊奇的优点,合并图片还降低了下载量,很多人会认为合并后的图片会比分离的图片的总和要大,因为合并后的图片中包含有附件的空白区域,但是实际上,它降低了图片自身的开销(颜色表、格式信息等等)。
如果需要在页面中为背景、按钮、导航栏、链接等提供大量图片,css sprites绝对是一种优秀的解决方案。——干净的标签、很少的图片和很短的响应时间。
内联图片
通过使用data:URL模式可以在web页面中包含图片但无需任何额外的HTTP请求。data:URL模式规范对它的描述为:允许将小块数据内联为立即数。数据就在其url自身之中,其格式如下:1
data:[<mediatype>][;base64],<data>
一个红色五角星形状的内联图片可以定义为:
该模式数据大小会有所限制。Base64编码会增加图片的大小,因此整体下载量会增加。
内联图片在跨越不同页面时不会缓存,所以不要去内联公司的logo图片,编码过的logo会导致页面变大。聪明的做法是使用css并将内联图片作为背景。将该css规则放在外部样式表中,这意味着数据可以缓存在样式表内部。
它和图片地图及css sprites的响应时间几乎一样,虽然外部样式表增加了一个额外的http请求,但被缓存后可以得到额外的收货。
合并脚本和样式表
前端工程师必须选择是对js和css进行“内联”(也就是将其嵌在html文档中)还是将其放在外部的脚本和样式表中。一般来说,使用外部脚本和样式表对性能更有利。然而,如果遵循模块化的原则将代码分开放到多个小文件中,会降低性能,因为每个文件都会导致一个额外的http请求。
在理想情况下,一个页面应该使用不多于一个的脚本和样式表。
将所有的javascript合并为一个单独的文件在开发环境中很难完成。解决
的方法是遵循编译型语言的模式。保持javascript的模块化,而在生成过程中从一组特定的模块生成一个目标文件。
合并文件是很容易的,难的是如果页面需要大量的模块,组合的数量就会非常庞大。每个页面不一定会使用到每个模块,这值得花时间去分析一下你的页面,确保组合的数量是管理的。
小结
遵守减少Http请求的规则可以同时改善首次浏览和后续浏览的网站响应时间。
使用内容发布网络
网站最初通常将其所有的服务器放在同一个地方。当用户群增加时,公司就必须面对服务器放置地点不再适用的事实,有必要在多个地理位置不同的服务器上部署内容。如果应用程序web服务器离用户更近,则一个http请求的响应时间将缩短。
内容发布网络(CDN)是一组分布在多个不同地理位置的web服务器,用于更加有效地向用户发布内容。向特定用户发布内容的服务器的选择基于对网路可用度的测量。例如,CDN可能选择网络阶跃数最小的服务器,或者具有最短响应时间的服务器。
除了缩短响应时间之外,CDN还可以带来其他优势。如备份、扩展存储能力、缓存、缓和web流量峰值压力。
CDN的缺点是你的响应时间可能会受到其他网站的影响,另外的缺点是你无法直接控制组件服务器所带来的特殊麻烦。例如,修改http响应头必须通过服务提供商来完成,如果CDN服务的性能下降了,你的工作质量也随之下降。
CDN用于发布静态内容,如图片、脚本、样式表和flash。提供动态html页面会引入特殊的存储需求,比如数据库连接、状态管理、验证、硬件和os优化等。这些复杂性超越了CDN的能力范围。
添加Expires头
规则3展示了如何配置组件,使其能够最大化地利用浏览器的缓存能力来改善页面的性能。
页面的访问者会进行很多http请求,但通过使用一个长久的expires头,使这些组件可以被缓存。长久的expires头最常用于图片,但应该将其用在所有组件上,包括脚本、样式表和flash。
Expires头
web服务器使用expires头来告诉web客户端它可以使用一个组件的当前副本,直到指定的时间为止。http规范中简要地称该头为“在这一日期/时间之后,响应将被认为是无效的”。它在http响应中发送。1
Expires: Thu, 15 Apr 2010 20:00:00 GMT
它告诉浏览器该响应的有效性持续到2010年4月15日为止。在到期前,浏览器在后续的页面浏览中会使用缓存的图片。
Max-Age和mod_expires
HTTP1.1引入了Cache-Control头来克服Expires头的限制。因为Expires头使用一个特定的时间,它要求服务器和客户端的时钟严格同步。另外,过期日期需要经常检查,并且一旦未来这一天到来了,需要再服务器配置中提供一个新的日期。
Cache-Control使用max-age指令指定组件被缓存多久。它以秒为单位定义了一个更新窗。如果从组件被请求开始过去的秒数少于max-age,浏览器就使用缓存的版本,这就避免了额外的Http请求。一个长久的max-age头将刷新窗设置为未来10年。1
Cache-Control: max-age=315360000
使用带有max-age的Cache-Control可以消除Expires的限制,你可以同时指定这两个响应头——Expires和Cache-Control max-age。如果两者同时出现,HTTP规范规定max-age指令将重写Expires头。
对于Expires带来的时钟同步和配置维护问题,mod_expires Apache模块使你在使用Expires头时能够像max-age那样以相对的方式设置日期。
下例中,过期时间被设计为自请求开始的10年之后:1
2
3<FilesMatch "\.(gif|jpg|js|css)$">
ExpiresDefault "access plus 10 years"
</FilesMatch>
它同时向相应中发送Expires头和Cache-Control max-age头。1
2Expires: Sun, 16 Oct 2016 05:43:02 GMT
Cache-Control: max-age=315360000
由于Cache-Control具有优先权,同时在HTTP1.0浏览器Expires生效。
不仅仅是图片
长久的Expires头应该包含任何不经常变化的组件,包括脚本、样式表和Flash组件。
修订文件名
如果我们将组件配置为可以由浏览器代理缓存,当这些组件改变时用户如何获得更新呢?当出现了Expires头时,直到过期为止一直会使用缓存的版本。
最有效的解决方案是修改其所有链接,这样,全新的请求将从原始服务器下载最新的内容。通常将版本号嵌在组件的文件名中。
总结
如果没有长久的Expires头,它仍然会存储在浏览器的缓存中。在后续请求中,浏览器会检查缓存并发现组件已经过期。为了提高效率,浏览器会发送一个GET请求。如果组件没有改变,原始服务器可以免于发送整个组件,而是发送一个很小的头,告诉浏览器可以使用其缓存的组件。
这些条件请求加起来,就是缓存节省的时间。
规则4——压缩组件
如果HTTP请求产生的响应包很小,传输时间就会减少,因为只需要将很小的包从服务器传递到客户端。
从HTTP1.1开始,Web客户端可以通过HTTP请求中的Accept-Encoding头来标识对压缩的支持。1
Accept-Encoding: gzip, deflate
如果Web服务器看到请求中有这个头,就会使用客户端列出的方法中的一种来压缩响应。Web服务器通过响应中的Content-Encoding头来通知Web客户端。1
Content-Encoding: gzip
gzip是目前最流行和最有效的压缩方法。支持deflate的浏览器也支持gzip,但很多浏览器支持gzip却不支持deflate。
压缩什么
压缩的内容包括XML和JSON在内的任何文本响应,但这里只关注脚本和样式表,因为他们用得最普遍。图片和PDF不应该被压缩,因为它们本来就已经被压缩了,会浪费CPU资源,还可能增加文件的大小。
服务器会花费额外的CPU周期来完成压缩,客户端要对压缩文件进行解压缩。要检测收益是否大于开销,需要考虑响应的大小、连接的带宽和客户端与服务器之间的Internet距离。这些信息难以得到且也有其他变数需要考虑。根据经验通常对1KB或者2KB以上的文件进行压缩。mod_gzip_minimum_file_size指令控制着希望压缩的文件的最小值。
节省
gzip是典型的压缩选择。gzip能将响应整体减少66%,而deflate能减少60%。
配置
Apache1.3使用mod_gzip,以下是最常用的指令:
启用mod_gzip
mod_gzip_on
基于文件类型、MIME类型、用户代理等定义哪些需要压缩、哪些不需要。
mod_gzip_item_include
mod_gzip_item_exclude
例如明确压缩脚本和样式表:
mod_gzip_item_include file .js$
mod_gzip_item_include mime ^application/x-javascript$
mod_gzip_item_include file .css$
mod_gzip_item_include mime ^text/css$
gzip命令行工具提供了一个选项,用于控制压缩的程度,可以在CPU使用量和数据大小的变化之间进行取舍,但mod_gzip中没有配置指令能够控制压缩级别。如果流式压缩产生的CPU负载成问题,可以考虑在磁盘或内存中缓存经过压缩的组件。mod_gzip提供了选项,可以将保存压缩过的内容自动保存在磁盘上,并在原内容发生变化时更新压缩过的内容。使用mod_gzip_can_negotiate和mod_gzip_update_static指令可以完成这一任务。
Apache2.x使用mod_deflate模块。对压缩脚本和样式表的基本配置:
AddOutputFilterByType DEFLATE text/html text/css application/x-javascript
和mod_gzip不同,mod_deflate包含了一个用于控制压缩级别的指令——DeflateCompressionLevel。
代理缓存
当浏览器通过代理来发送请求时,情况就变得复杂了。假设某url发送到代理的第一个请求来自于一个不支持gzip的浏览器。这是到达代理的第一个请求,因此缓存为空。代理会将请求转发到web服务器。此时服务器的响应是未经过压缩的。这个没有压缩的响应被代理缓存起来并发送给浏览器。假设到达代理的第二个请求是来自同一个url,来自于一个支持gzip的浏览器。代理会使用缓存中(未经压缩)的内容进行响应,这就失去了进行压缩的机会。如果顺序反了,情况可能更加严重。
解决的方法是在web服务器的响应中添加Vary头,告诉代理根据一个或多个请求头来改变缓存的响应。因此需要在服务器的Vary响应头中包含Accept-Encoding.
Vary: Accept-Encoding
默认情况下,mod_gzip会向所有响应添加Vary: Accept-Encoding头。
浏览器边缘情形
不是所有浏览器都完美支持压缩
在Apache1.3可以通过在mod_gzip_item_include中使用恰当的User-Agent值来指定浏览器白名单:
mod_gzip_item_include reqheader “User-Agent: MSIE [6-9]”
mod_gzip_item_include reqheader “User-Agent: Mozilla/[5-9]”
在Apache2.x使用BrowserMatch指令
BrowserMatch ^MSIE [6-9] gzip
BrowserMatch ^Mozilla/[5-9] gzip
将代理缓存加进来,把User-Agent作为代理的另外一种评判标准添加到Vary头中。
Vary: Accept-Encoding,User-Agent
mod_gzip检测到你在使用浏览器白名单时,会自动将User-Agent字段添加到Vary头中,不幸的是,由于User-Agent有上千种值,代理不太可能为其所代理的所有url缓存Accept-Encoding和User-Agent的所有组合。甚至这样可能会导致完全禁用为响应包进行的缓存。另外一种方法是使用Vary: 或Cache-Control: private头来禁用代理缓存。因为Vary: 头防止了浏览器使用缓存的组件,不过最好使用Cache-Control: private,Google和Yahoo都使用了这种方式。
如何平衡压缩和代理支持的决定是很复杂的,需要在加快响应时间、减小带宽开销和边缘情形浏览器缺陷之间进行权衡。
规则5——将样式表放在顶部
我们发现将DHTML特征的样式表放在文档顶部——head中——能使页面加载得更快。
逐步呈现
关心性能的前端工程师都希望页面能逐步地加载,为用户提供可视化的反馈是很重要的。
将样式表放在文档底部,浏览器为避免当样式变化时重绘页面中的元素,浏览器会阻塞内容逐步呈现。浏览器延迟显示任何可视化组件,就会出现“白屏“。
将CSS放在底部
在Internet Explorer中,将样式表放在文档底部会导致白屏的问题情形如下:
1.在新窗口中打开时
2.单击刷新按钮,在页面加载时最小化然后恢复窗口就能看到白屏。
3.设置为主页打开新的浏览器窗口时
将CSS放在顶部
若将样式表放在文档顶部的HEAD中,页面都是逐步呈现的
关于Link标签和@import规则1
<link rel="stylesheet" href="styles1.css">
1 | <style> |
一个style块可以包含多个@import规则,但必须放在所以其他规则之前。同时@import规则有可能会导致白屏,即使是放在HEAD标签中。使用@import有时会导致组件下载的无序性。
无样式内容的闪烁
白屏现象源自于浏览器的行为。如果样式表仍在加载,构建呈现树就是一种浪费,因为在所有样式表加载并解析完毕之前显示内容会遇到FOUC(无样式内容的闪烁,Flash of Unstyled Content)问题
白屏是对FOUC问题的弥补。
IE在前面介绍的情形中会选择白屏,在单击链接、使用书签或键入URL会选择第二种方式——承担FOUC风险。
规则6——将脚本放在底部
脚本带来的问题
脚本会阻塞并行下载。
并行下载
HTTP1.1规范,建议浏览器从每个主机名并行下载两个组件。当然这个在浏览器上是可以重新配置的。使用CNAME(DNS别名)可以将组件放在多个主机中。增加并行下载并非没有开销,取决于带宽、CPU速度,过多的并行下载反而会降低性能。
脚本阻塞下载
在下载脚本时并行下载实际是被禁用的,其中一个原因是脚本可能会使用document.write来修改页面内容,因此浏览器会等待,以确保页面能够恰当的布局。
另一个重要的原因是保证脚本能够按照正确的顺序执行。
最差情况:将脚本放在顶部
影响:
- 脚本会阻塞对其后面内容的呈现
- 脚本会阻塞对其后面组件的下载
正确地放置
在很多情况下,很难将脚本移到底部。例如,如果脚本使用document.write向页面中插入了内容,就不能将其移动到页面中靠后的位置。此外还会有作用域问题。很多情况下,可以用其他方法解决这些情形。
经常出行的另外一种建议是使用Defferred脚本。DEFER属性表明脚本不包含document.write,浏览器得到这一线索可以继续呈现。但是在firefox中即使是延迟脚本也会产生阻塞。如果一个脚本可以延迟,那么它一定可以移到页面底部,这是最佳方式。
避免CSS表达式
使用CSS表达式将背景色设置为每小时变化一次:1
backgroud-color: expression( (new Date()).getHours() % 2 ? "#FFF" : "#000" );
对于低版本IE不支持min-width,可以识别表达式,而其他浏览器识别静态配置:1
2width: expression( document.body.clientWidth < 600 ? "600px" : "auto" );
min-width: 600px;
更新表达式
表达式的问题在于对其进行的求值的频率比人们期望的要高。它们不只是页面呈现和大小改变时求值,当页面滚动、甚至用户鼠标在页面上移过时都要求值。
围绕问题展开工作
用两种技术可以避免css表达式产生这一问题——创建一次性表达式和使用事件处理器取代css表达式。
一次性表达式
1 | <style> |
css表达式调用了altBgcolor()函数,而该函数将样式的backgroud-color属性设置为一个明确的值,并移除了CSS表达式。
事件处理器
当浏览器的大小改变时,这个例子使用setMinWidth()函数来修改所有段落元素的大小1
2
3
4
5
6
7
8
9function setMinWidth() {
var aElements = document.getElementsByTagName("p");
for (var i = 0; i < aElements.length; i ++) {
aElements[i].runtimeStyle.width = ( document.body.clientWidth < 600 ? "600px" : "auto" );
}
}
if (1 != navigator.userAgent.indexOf("MSIE")) {
window.onresize = setMinWidth;
}
但这在第一次呈现时并不能恰当的设置段落的大小,所以需要使用”一次性表达式“介绍得方法设置初始宽度。
规则8——使用外部javascript和css
内联vs外置
纯粹而言,内联快一些,这主要是因为外部示例需要承担多个HTTP请求带来的开销。
但是JavaScript和css文件外置有机会被浏览器缓存起来。
如果你的网站的本质上能够为用户带来高完整缓存率,使用外部文件的收益就更大。如果不太可能产生完整缓存,则内联是更好的选择。
组件重用
如果你的网站中每个页面都使用了相同的javascript和css,使用外部文件可以提高这些组件的重用率。在这种情况下使用外部文件更加具有优势,因为当用户在页面间导航时,javascript和css组件已经位于浏览器的缓存中了。
在典型情况下,页面之间javascript和css的重用既不可能100%重叠,也不可能100%无关。
最好的答案就是折中,将你的页面划分成几种页面类型,然后为每种类型创建单独的脚本和样式表。这比维护一个单独的文件要复杂,但通常比为每个页面维护不同的脚本和样式表要容易,并且对于给定的任意页面都只需要下载很少的多余的javascript和css。
主页
我所见过的使用内联方式反而更好的一个例外是主页。
页面查看
主页拥有没有很高的页面查看数量,然而,通常每个会话只有一个页面查看。
空缓存vs完整缓存
完整缓存的百分比要比其他网站更低。出于安全的原因,很多用户选择在每次关闭浏览器时清空缓存。下一次用户打开浏览器时,产生的是一个主页的空缓存页面查看。
组件重用
重用率很低,很多主页是用户来到网站后访问的唯一一个页面,因此它们谈不上重用。
分析了这些基准,我们更加倾向于适用内联,当然,没有适用于所有主页的唯一答案。
两全其美
这里介绍得两项技术使你既可以获得内联的优势,同时也能缓存外部文件。
加载后下载
对于作为多次页面查看中的第一次的主页,我们希望为主页内联javascript和css,但又能为所有后续页面查看提供外部文件。这可以通过在主页加载完成后动态下载外部组件来实现(通过onload事件)。这能够将外部文件放到浏览器的缓存中以便用户接下来访问其他页面。
这些页面中javascript和css被加载到页面中两次(先是内联的,然后是外部的)。要使其能够工作,必须处理双重定义。例如脚本,可以定义但不能执行函数(至少不能让用户察觉)。使用了相对单位(百分比或em)的css如果指定两次可能会产生问题。将这些组件放到一个不可见的IFrame中是一种更好的方式。
动态内联
这可以通过使用前一个例子中的加载后下载技术来完成。当用户第一次访问页面时,服务器发现没有cookie,于是生成一个内联了组件的页面。然后服务器添加javascript来在页面加载后动态下载外部文件(并设置cookie)。下一次访问页面时,服务器看到了cookie,就会生成一个使用外部文件的页面。
这种方式的美好之处在于它的宽容。即便cookie的状态和缓存的状态不匹配,页面也能够工作,只是没有本应该的那么优化而已。
减少DNS查找
DNS将主机名映射到IP地址上,在DNS查找的响应时间依赖于DNS解析器(通常由你的ISP提供),它所承担的请求压力、你与它之前的距离和你的带宽速度。
DNS缓存和TTL
DNS查找可以被缓存起来以提高性能。这种缓存可以发生在由你的ISP或者局域网中的一台特殊的缓存服务器上,但我们这里要探索的是发生在独立用户的计算机上的DNS缓存。
例如,在用户请求了一个主机名之后,DNS信息会留在操作系统的DNS缓存中,之后对于该主机名的请求将无需进行过多的DNS查找,至少短时间内不需要。
很多浏览器拥有其自己的缓存,和操作系统的缓存相分离。只要浏览器在其缓存中保留了DNS记录,它就不会麻烦操作系统来请求这个记录。只有当浏览器缓存丢失了记录时,它才会向操作系统询问地址——然后操作系统或者通过其缓存来响应这个请求,或者将请求发送给一台远程服务器,这时就会发生潜在的速度降低。
设计者知道IP地址会变化以及缓存会消耗内存。因此,应该周期性地清除缓存中的DNS记录,并通过大量不同的配置设置检测清除的频率有多高。
影响DNS缓存的因素
服务器可以表明记录可以被缓存多久。查找返回的DNS记录包含了一个存活时间(Time-to-live,TTL)值。该值告诉客户端可以对该记录缓存多久。
尽管操作系统缓存会考虑TTL值,但浏览器通常忽略该值,并设置它自己的时间限制。此外Keep-Alive特性可以同时覆盖TTL和浏览器的时间限制。只要浏览器和web服务器通信着,并保持TCP连接打开的状态,就没有理由进行DNS查找。
浏览器对缓存的DNS记录的数量也有限制,而不管缓存记录的时间,访问较多的域名,较早的DNS记录将被丢弃。不过操作系统可能依然保留着该记录,这能扭转一下局面。
TTL值
不同网站的TTL值差距很大,许多网站都在努力做到当服务器、虚拟IP地址(VIP)或联合定位掉线时提供快速故障转移。这也是提供较多TTL的原因。MySpace定位到一个联合定位工具,对于其当前的网络拓扑,故障转移并不是很重要,因此他们选择了较长的TTL。
客户端收到的DNS记录的平均TTL值只有最大TTL值的一半。这是因为DNS解析器自身也拥有与DNS记录相关的TTL。当浏览器进行DNS查找时,DNS解析器返回的时间是其记录的TTL的剩余时间。如果最大TTL是5分钟,DNS解析器返回的TTL范围可能是1~300秒。
浏览器视角
Microsoft Windows上的DNS缓存由DNS Client服务进行管理。你可以使用ipconfig命令来查看和刷新DNS Client服务。1
2ipconfig /displaydns
ipconfig /flushdns
重新启动也可以清空DNS Client服务缓存。重新启动浏览器会清空浏览器缓存,但不会清空DNS Client服务缓存。
IE
IE的DNS缓存由三个注册表设置控制
部分设置的默认值值如下:
DnsCacheTimeout——30分钟
KeepAliveTimeout–1分钟
ServerInfoTimeout–2分钟
如果DNS服务器TTL值小于30分钟的话,对浏览器进行DNS查找的频率产生的影响很小。一旦浏览器缓存了DNS记录,就会使用30分钟作为TTL值。如果发生了错误,刷新DNS查找就会比这要快,在正常情况下,很短的TTL值(30min以下)在IE中不会增加DNS查找的数量。
Keep-Alive,一个持久的TCP连接将会一直使用,直到其空闲1分钟为止。由于连接是持久的,因此无需DNS查找。
ServerInfoTimeout的值为2分钟,说明尽管没有Keep-Alive,如果一个主机名每两分钟重用了一次,并且没有发生错误,也无需尽心DNS查找。
当网络操作中心尝试通过DNS变化来转移流量时,如果一个IP上的流量已经被转移走,但该IP仍在运行,则使用旧的DNS记录的IE用户至少需要30分钟才能更新DNS。至少每两分钟访问一次的活跃用户会一直使用旧的IP,直到发生错误。
Firefox
具有下列配置设置
1.network.dnsCacheExpiration-1分钟
2.network.dnsCacheEntries-20
3.network.http.keep-alive.timeout-5分钟
减少DNS查找
当客户端的DNS缓存为空(浏览器和操作系统都是)时,DNS查找的数量与web页面中唯一主机名的数量相等。减少唯一主机名的数量就可以减少DNS查找的数量。
减少唯一主机名的数量会潜在地减少页面中并行下载的数量。
我的建议是将组件分别放到至少2个,但不要超过4个主机名下。
另外,确保服务器支持Keep-Alive还能减少DNS查找。
规则10——精简javascript
精简
精简是从代码中移除不必要的字符以减少其大小,进而改善加载时间的实践。在代码被精简后,所有的注释以及不必要的空白字符(空格、换行和制表符)都将被移除。
这里,我需要谈一下另外一种更具挑战性的精简方式——混淆。
混淆
混淆和精简一样,它也会移除注释和空白,同时它还会改写代码。作为改写的一部分,函数和变量的名字将被转换为更短的字符串,这时的代码更加精炼,也更加难以阅读。通常这样做是为了增加对代码进行反向工程的难度,但这对提高性能也有帮助,因为这比精简更能减小代码的大小。
精简是一个安全并且相当简单的过程,而混淆则更为复杂一些。混淆有三个主要的缺点——
缺陷 混淆过程本身很有可能引入错误
维护 由于混淆会改变javascript符号,因此需要对任何不能改变的符号(例如API函数)进行标记,防止混淆器修改它们。
调试 经过混淆的代码很难阅读。这使得在产品环境中调试问题更加困难。
我的建议是使用精简而不是混淆。最终的决定需要考虑混淆能够带来的额外的代码大小减少量。
节省
这里使用JSMin和Dojo Compressor(已改名ShrinkSafe)精简javascript代码。
我们精简来自YUI库的event.js。
经过JSMin的处理后,所有不必要的空白将被移除
Dojo Compressor移除了大部分的空白,同时还缩短了变量名。
混淆相比精简可以进一步减小代码尺寸,结合了gzip压缩后,之间的差别将会减小。
锦上添花
内联脚本也可以精简
精简css
精简css能够带来的节省通常要小于精简javascript,因为通常css中的注释和空白比javsscript少。最大的潜在节省来自于优化css-合并相同的类、移除不使用的类等。css的依赖顺序的本质决定了这将是一个复杂的问题。这个领域还需要进一步研究和工具开发。最佳的解决方案还是移除注释和空白,并进行一些直观的优化,比如使用缩写(”#606”代替”#660066”)和移除不必要的字符串(”0”代替”0px”)
规则11——避免重定向
重定向用于将用户从一个URL重新路由到另一个URL。重定向有很多种——301和302是最常用的两种。重定向可能有很多不同的原因,包括网站重新设计、跟踪流量、记录广告点击和建立易于记忆的URL。
重定向的类型
当web服务器向浏览器返回一个重定向时,响应中就会拥有一个范围在3xx的状态码。这表示用户代理必须执行进一步操作才能完成请求。
300 Multiple Choices(基于Content-Type)
301 Moved Permancently
302 Moved Temporarily(亦称Found)
303 See Other(对302的说明)
304 Not Modified
305 Use Proxy
306 (已废弃)
307 Temporary Redirect (对302的说明)
“304 Not Modified”并不真的是重定向——它用来响应条件GET请求,避免下载已经存在于流量器缓存中的数据
301和302是使用得最多的。状态码303和307是在http1.1规范中添加的,用来澄清对302的使用(滥用),但是几乎没有人用303和307,绝大多数网站仍然在沿用302.
下面是一个301响应头的一个示例
浏览器会自动将用户带到Location字段所给出的URL。重定向所必需的所有信息都出现在这个头中了。响应体通常是空的。不管叫什么名字,301和302响应在实际中都不会被缓存,除非有附加的头——如Expires或Cache-Control等——要求它这么做。
还有其他方法可以自动将用户重定向到其他URL。HTML文档的头中包含的meta refresh标签可以在其content属性所指定的秒数之后重定向用户1
<meta http-equiv="refresh" content="0; url=http://strbesouders.com/newuri">
javascript也可以用于执行重定向,将document.location设置为期望的url即可。如果你必须进行重定向,最好的技术是使用标准的3xx HTTP状态码,这主要是为了确保后退按钮能够正确工作。
重定向之外的其他选择
缺少结尾的斜线
很多web开发人员没有意识到,在url的结尾必须出现斜线(/)而没出现时,如http://astrology.yahoo.com/astrology会导致一个301响应, 包含了一个到http://astrology.yahoo.com/astrology 的重定向。当缺少结尾的斜线时发送重定向有着很充分的理由——它允许自动索引(autoindexing,自动转到默认的index.html上)并且能够获得与当前目录相关的url(如logo.gif)。然而,很多流行的web页面并不依赖自动索引,而是依赖特定的url和处理器。另外,url通常也与根目录相关而不是和当前目录相关。
注意当主机名后缺少结尾斜线时不会发生重定向。例如,http://www.yahoo.com 不会产生重定向。然而,你在浏览器中看到的最终的url是包含结尾斜线的——http://www.yahoo.com/ .导致自动产生结尾斜线的原因是,浏览器在运行get请求时必需指定一些路径。如果没有路径,例如http://www.yahoo.com 它就会简单地使用文档根(/)
GET / HTTP 1.1
当缺少结尾斜线时发送重定向是很多web服务器的默认行为,包括Apache. Alias指令是一种简单的方法。另一种选择是使用mod_rewrite模块,但Alias更加简单。(注1)Astrology网站的问题可以通过向Apache配置中添加下列内容来解决——
Alias /astrology /user/local/apache/htdocs/astrology/astrology.html
如果使用Apache2.0中的处理器,一种更为清晰的解决方案是使用DirectorySlash指令。假设有一个名为astrologyhandler的处理器,可以像下面这样使用DirectorySlash1
2
3
4<Localtion /astrology>
DirectorySlash Off
SetHandler astrologyhandler
</Localtion>
这些方法都不能解决查找与当期目录相关的url问题,因此页面中组件的url必须与根目录相关。而且,你还必须知道各模块运行的顺序(尤其是mod_dir和mod_autoindex),因为这样使用DirectorySlash可能会有安全隐患。
总之,如果你的网站包含目录并使用了自动索引,用户就必须忍受一个到达预期页面的重定向。检查一下你的web日志就能看到发出了多少301状态,这能帮助你认识到多么值得去解决缺少结尾斜线的问题。
连接网站
想象一下网站后端被重写的情形。这经常发生,新的实现中的url很可能会有所不同。将用户从旧的url转移到新的url的最简单的方式就是重定向。重定向是使用定义良好的API——url来整合两个代码基础的一种方式。
将旧网站连接到新网站只是重定向这种常见应用中的表现形式之一。其他形式还包括将一个网站的不同部分连接起来,以及基于一些条件(浏览器类型、用户账户类型等)来引导用户。使用重定向来连接两个网站很简单而且只需要很少的额外代码。
其实整合后端还有其他的选择,但比重定向需要更多的开发工作,不过这样不会损害用户体验。
1.Alias、mod_rewrite和DirectorySlash要求除url之外还要提交到一个接口(处理器或文件名),但易于实现。
2.如果两个后端位于同一台服务器,则它们的代码很可能自己就能连接。例如,旧的处理器代码可以通过编程调用新的处理器代码。
3.如果域名变了,可以使用一个CNAME(一条DNS记录,用于创建从一个域名指向另一个域名的别名)让两个主机名指向相同的服务器。如果能做到这一点,这里提到的技术(Alias、mod_rewrite、DirectorySlash和直接连接代码)就是可行的。
跟踪内部流量
重定向经常用于跟踪用户流量的流向。例如,sports链接的url是http://www.yahoo.com/r/26.单击这个链接将产生301响应,其Location被设置为http://sports.yahoo.com/。通过分析来自www.yahoo.com的web服务器日志可以得知人们离开Yahoo!的首页后的流量去向。
另一种选择是使用Referer日志来跟踪流量去向。每个http请求都包含一个url,表明从哪个页面发起的请求,也就是引用方(有的时候没有引用页,当用户键入url或使用书签时)。在这里,当用户从Yahoo!主页导航到Sports页时,sports.yahoo.com的访问日志中会包含一个Referer,其值为http://www/yahoo.com/。
跟踪出站流量
出站流量使用referer就不太现实了
Yahoo!search目前将每个搜索结果链接包装到一个重定向中来解决跟踪的问题。搜索结果的url都指向rds.yahoo.com并将最终的目标当作参数包含在该url中。例如,下面是指向wikipedia的“Performance”词条的搜索结果链接
http://rds.yahoo.com/[...]5742/**http%3a/en.wikipedia.org/wiki/Performance
单击这个搜索结果会访问rds.yahoo.com,它将返回一个302响应,其Location被设置为http://en.wikipedia.org/wiki/Performance.管理员通过分析rds.yahoo.com上的web服务器日志的**参数就能跟踪用户去了哪里。这个重定向使得获取目标页面变慢了。
除了重定向,还可以选择信标——一个http请求,其url中包含有跟踪信息。跟踪信息可以从信标web服务器的访问日志中提取出来。信标响应通常是一个1px*1px的透明图片;不过204响应更为优秀,因为它更小,从来不会被缓存,而且绝对不会改变浏览器状态。
在Yahoo!search,目标是无论何时用户单击搜索结果链接时都要发送一个信标。这通过为每个连接提供onClick处理器来完成(当启用了javascript时)。onclick处理器将调用一个函数,请求一个图片,并在图片的url中包含要跟踪的信息。
这种情况下,挑战是发送信标和页面自身被卸载之间的竞态情形。图片信标的onload处理器可以用于确保在卸载文档之前信标应传送完毕。
这种方法可能和使用重定向一样慢,另一种方式是使用XMLHttpRequest来发送信标,但在卸载页面之前只需等请求到达readyState 2即可。这比等待重定向的整个http响应要快,但你必须决定是否有必要采取这么复杂的方式。
美化URL
使用重定向的另一种动机是使url更加美观并且易于记忆。如http://www.google.com/tools/firfox/toolbar/FT3/intl/en/index.html -> http://www.google.com.
但是,使用alias、mod_rewrite、DirectorySlash和直接链接代码来避免重定向也能达到目的。
规则12——移除重复脚本
导致一个脚本的重复有两个主要因素——团队大小和脚本数量
重复脚本损伤性能的方式有两种——不必要的http请求和执行JavaScript所浪费的时间。
避免重复脚本
第一种方法是,实现一个脚本管理模块。包含脚本的典型方式是在html页面中使用script标签——1
<script type="text/javascript" src="menu_1.0.17.js"></script>
另一种选择是在php中创建一个称作insertScript的函数
<?php insertScript(“menu.js”) ?>
规则13——配置ETag
Etag是什么?
实体标签是web服务器和浏览器用于确认缓存组件的有效性的一种机制。
发送条件get请求是,服务器在检测缓存的组件是否和原始服务器上的组件匹配时有两种方式——
1.比较最新修改日期
2.比较实体标签
原始服务器通过Last-Modified响应头来返回组件的最新修改日期
下一次请求是浏览器会使用If-Modified-Since头将最新修改日期传回到原始服务器以进行比较。如果匹配,会返回一个304 Not Modified
实体标签
ETag提供了另外一种方式,Etag在http1.1中引入,标识了一个组件的一个特定版本的字符串。唯一的格式约束是该字符串必须用引号引起来。原始服务器使用Etag响应头来指定组件的Etag
Etag的加入为验证实体提供了比最新修改日期更为灵活的机制。例如,如果实体依据User-Agent或Accept-Language头而改变,实体的状态可以反映在Etag中。
此后,浏览器会使用If-None-Match头将ETag传回原始服务器。如果Etag是匹配的,就会返回304状态码。
ETag带来的问题
Etag的问题在于,通常使用组件的某些属性来改造它,这些属性对于特定的、寄宿了网站的服务器来说是唯一的。当浏览器从一台服务器上获取了原始组件,之后,又向另外一台不同的服务器发起条件GET请求时,Etag是不会匹配的——而对于使用服务器的集群来处理请求的网站来说,这是很常见的一种情况。
Apache1.3和2.x的ETag格式是inode-size-timestamp。文件系统使用inode来存储诸如文件类型、所有者、组合访问模式等信息。尽管在多台服务器上一个给定的文件可能位于相同的目录、具有相同的文件大小、权限、时间戳等,从一台服务器到另一台服务器,其inode仍然是不同的。
IIS5.0和6.0在Etag上有类似的问题。IIS上Etag的格式是filetimestamp:changeNumber。changeNumber适用于跟踪IIS配置变化的计数器。对于一个网站背后的所有IIS服务器来说,changeNumber不大可能相同。
最后的结果是对于完全相同的组件,从一台服务器到另一台,apache和IIS产生的Etag是不会匹配的。如果Etag不匹配,用户就不会按照Etage的设计计划那样接收到更小更快的304响应。
If-None-Match比If-Modified-Since具有更高的优先级。如果请求中同时出现了这两个头,则原始服务器“禁止返回304”,除非请求中的条件头字段一致。
Etag——用还是不用
一种选择是对Etag进行配置,以利用其灵活的验证能力。例如可以使用一段根据浏览器是否为IE而变化的脚本。如果使用PHP来生成脚本,你可以通过设置Etag头来反映浏览器状态。1
2
3
4
5
6
7
8
if ( strops($_SERVER["HTTP_USER_AGENT"], "MSIE") ) {
header("ETag: MSIE");
}
else {
header("Etag: notMSIE");
}
如果你的组件必须通过最新修改日期之外的一些东西来进行验证,则Etag是一种强大的方法。
如果你无须自定义Etag,最好简单地将其移除。Apache和IIS都将Etag视为一个性能问题,并建议修改Etage的内容。
Apache1.3.23版和之后版本支持FileEtag指令。使用这一指令,可以从ETag中移除inode值,只留下大小和时间戳作为组件的Etag。类似的,在IIS中可以为所有服务器设置相同的ChangeNumber,保留文件的时间戳作为Etag中仅有的另一块信息。然而Last-Modified头可以提供完全等价的信息,而且移除Etag可以减小响应和后续请求的Http头的大小。
在Apache中,只需向Apache配置文件中简单地添加下面一行配置就能移除Etag——
FileEtag none
规则14——使Ajax可缓存
Ajax
术语ajax由Jesse James Garrett于2005年提出,Ajax表示“异步的JavaScript和XML”。Ajax不是一个单独的,需要许可证的技术,而是一组技术,包括JavaScript、CSS、DOM和异步数据获取。Ajax的目的是为了突破Web本质的开始-停止交互方式。向用户显示一个白屏然后重绘整个页面不是一种很好地用户体验。而Ajax在UI和Web服务器之间插入了一层。这个Ajax层位于客户端,与Web服务器进行交互以获取请求的信息,并与表现层交互,仅更新那些必要的组件。它将web体验从“浏览页面”转变为“与应用程序进行交互”。
现实世界中的Ajax缓存
重复请求两次Yahoo!Mail消息列表,并没有被缓存,没有被缓存的原因是响应中包含一个值为no-store的Cache-Control头,以及一个日期为过去某一天的Expires头。这些都告诉浏览器不要缓存响应。
使这些Ajax请求可缓存,除了改变HTTP头之外还需要进行更多的工作。响应的个性化和动态本质必须被反映到缓存中。可供采用的最好的方式是使用查询字符串参数。例如,这个响应只对当前用户有效。可以将用户名放到查询字符串中来做到这一点——1
/ws/mail/v1/formrpc?m=GetMessage&yid=steve_souders&msgid=001234
该响应可能会因为数据隐私原因而不能缓存。当数据被认为是私有的时,大多会使用Cache-Control: no-store。在使用了这个头之后,响应根本就不会被写入到磁盘上。但是,HTTP规范警告说不要依赖这一机制来确保数据的隐私性,恶意的或危险的缓存会完全忽略Cache-Control: no-store头。
处理数据隐私性的另外一个更好的方式是使用安全通信协议如SSL。SSL响应是可缓存的,因此它提供了一种妥协——在确保数据隐私的同时在当前会话中缓存响应以改善用户体验。
和Yahoo!Mail示例类似的例子,缓存一个电子表格,如果用户修改了该电子表格,我们必须确保产生变化后不会再使用缓存的请求。简单的解决方法还是使用查询字符串。Googole Spreadsheets的后端应该具有一个时间戳,来表示末次修改发生的时间,并将其嵌入到Ajax请求的查询字符串中——
/ar?id=[snip…]&srow=0&erow=100&t=1177458941