Archive

Posts Tagged ‘high performance web sites’

<high performance web sites> Put Stylesheets at the top

September 15th, 2009 No comments

Rule5: Put Stylesheets at the top
在yahoo!一个运行门户的团队在页面上增加一些DHTML的特性,前提是他们尽量保证这不会对页面的响应时间有不利影响. 在那些复杂的DHTML特性中,一个用来发送email消息的弹出div,它不是真正的属于此页面的渲染部分. 这是因为他只有在页面已经加载完成并且用户点击发送email消息的button后才能被访问. 既然它对页面的渲染是无用的,那么前台后端工程师把弹出框中的CSS放到一个外部的stylesheet中,然后通过在页面底部的一个链接tag来加载, 期望达到在页面最后加载,加快页面加载.
这么做背后的逻辑是有意义的. 许多其他的组件(images, stylesheets, scripts, etc.)是页面渲染所需要的。 因为组件都是按照页面文档中出现的先后顺序来下载的, 把这些DHTML的特性使用的stylesheet放到最后可以使更多关键的组件先于下载,结果就是加快页面加载.
事情真的会这样么?
在Internet Explorer中,页面的加载速度比原来的设计明显慢一些. 在解决这个问题加速页面加载的过程中,我们发现把DHTML特性的stylesheet放到页面文档的顶上,在HEAD里时,页面加载会更迅速. 这和我们期望的是相反的. 为什么把stylesheet放在首位,这样延迟页面关键组件的加载, 实际上提高页面加载时间呢? 更多的研究引出Rule 5,这个规则.

Progressive Rendering
前台后端工程师关心性能想让页面的加载能够进步; 换句话说, 我们想让浏览器显示任何内容都可以尽可能的快. 这对含有大量内容的页面或者在低网速条件下的用户来说是尤为重要的. 用户视觉反馈已经被研究和证明是非常重要的. Jakob Nielson,可用性工程师的先锋,在他的文章中强调视觉反馈是一个期间内重要的进步指标.
Progress indicators have three main advantages: They reassure the user that the system has not crashed but is working on his or her problem; they indicate approximately how long the user can be expected to wait, thus allowing the user to do other activities during long waits; and they finally provide something for the user to look at,thus making the wait less painful.This latter advantage should not be underestimated and is one reason for recommending a graphic progress bar instead of just stating the expected remaining time in numbers.
在我的例子里HTML页面是进步指标. 当浏览器加载页面进行的时候, 页面头,导航栏, 顶上的logo等等,都作为视觉反馈为等待页面额的用户提供服务. 这提高了整体的用户体验.
把stylesheets放到页面文档的底部的问题是这样做在很多浏览器中阻止了页面进一步的渲染. 浏览器阻滞渲染为了避免重画页面上的元素, 假如他们的styles改变. Rule 5对实际的加载那些页面组件的时间起很小的作用,但是对浏览器对组件的重新排序起较大作用. 事实上, 页面的视觉组件加载快,而又感觉慢的页面是具讽刺义的. 浏览器延迟显示一些视觉组件因为它和用户都要等待页面底部的stylesheet. 接下来的小节的例子将示范这个现象,我称他为”blank white screen”.

sleep.cgi
在建立模拟这个现象的例子的时候, 我开发了一个工具, 我已经发现的极其有效的显示组件如何延迟影响web页面: sleep.cgi. 这是一个简单的Perl CGI程序, 使用的paramters如下:
sleep
用来控制response的延迟时间,单位是秒. 默认值为0.
type
返回组件的类型. 可能的值是gif, js, css, html and swf. 默认值是gif.
expires
三选一的值: -1(返回一个已经过期的Expire Header头信息), 0(返回无Expire Header头信息), 1(返回一个在未来过期的Expire Header头信息). 默认值是1.
last
值-1返回一个最后修改时间Last-Modified header,并且这个时间和file的时间相同. 值0 不返回Last-Modified header. 默认值是-1.
redir
值1返回一个302的的响应,返回的URL和请求的URL除了少了redir=1外,其他完全相同.
第一个例子需要一些慢的images 和 慢的stylesheet. 我们可以通过sleep.cgi构造以下请求实现:

       <img src="/bin/sleep.cgi?type=gif&sleep=2&expires=-1&last=0">
       <link rel="stylesheet" href="/bin/sleep.cgi?type=css&sleep=1&expires=-1&last=0">

image和stylesheet这两个都使用了expires=-1的参数, 这样response返回的Expires header是过期的. 这就防止这两个组件被cache, 以便你可以重复运行测试并且在每次都能得到相同的体验(我也在每个组件的url上添加了唯一的时间戳,防止缓存). 为了减少测试的变数,我特别指定了last=0来移除response里的Last-Modified header信息. 这里image请求有两秒的延迟(sleep=2), 而stylesheet只有1秒的延迟(sleep=1). 这可以保证任何的延迟看起来都不是针对于stylesheet的影响时间, 而是针对于阻止行为(就是这个页面要测试的内容).
为了能够夸大组件的response时间,使得这个过程尽可能的被显现. 我已经将sleep。cgi的Perl的源代码公开,这样其他人可以用它来做自己测测试(http://stevesouders.com/hpws/sleep.txt). 拷贝里面的代码到sleep.cgi的可执行文件中,并把这个文件放到你web server中的可执行目录中.

Blank White Screen
这部分内网将描述两个页面,不同点在于:是否把stylesheet放到的头部或者底部. 这样会使用户体验有何不同!

CSS at the Bottom
第一个例子我们示例了把stylesheet放到页面文档底部的伤害.
CSS在底部
http://stevesouders.com/hpws/css-bottom.php
注意把sytlesheet放到文档的底部可以延迟页面的加载. 这个问题很难被追踪到, 它只发生在Internet Explorer 并且依赖页面的加载方式. 当你试过这个页面后,你会注意到这个页面的加载明显变慢了. 当这发生的时候,此页面是完全空白的,直到所有的内容瞬间突然出现在屏幕上. 页面渲染的进展已经被阻碍. 这是非常差的用户体验,因为这里没有视觉反馈使用户放心她的请求已经被正常的处理. 相反的, 用户只有怀疑是否发生了意外. 这时候,用户放弃了你的网站并且转向到你竞争者的网站.
这里有三种情况的测试,把stylesheet放到文档的底部在IE浏览器中出现空白屏的现象.
In a new window
在刚测试空白屏的页面中,点击”new window”的链接,打开新窗口. 用户经常打开新窗口当她们要另外的网站的时候,比如在搜索结果页面到一个实际的目标页面.
As a reload
点击浏览器的Refresh按钮.这是一个正常的用户行为,这也是一种能引起空白屏的方式. 最小化和恢复窗口当页面加载时看到空白屏.
As a home page
设置浏览器的默认首页为http://stevesouders.com/hpws/css-bottom.php, 打开浏览器后就会引起空白屏. Rule 5对于想让用户把他的页面当作首页的团队来说是相当重要的.

CSS at the Top
为了避免空白屏的发生, 把stylesheet放到文档的头部. 我做了一个例子页面”css at the top”解决了所有情节的问题. 无论是在新窗口或者刷新或者作为一个主页的时候,页面渲染都正常进行.
css at the top
http://stevesouders.com/hpws/css-top.php
解决了前面提出的问题点.

html文档中有两种添加stylesheet方式: 用LINK tag 和@import rule. 一个LINK tag的例子如下:

       <link rel="stylesheet" href="styles1.css">

一个使用@import rule的例子如下:

       <style>
         @import url("styles2.css");
       </style>

一个style块可以含有多个@import rule,但是@import rule必须在其他规则之前. 我曾经看到过一个例子, 所有的开发者花费时间去查找为什么一个使用@import rule的stylesheet没有被加载. 碰到这样的情况,我宁愿使用LINK tag. 除了简单的语法外,使用LINK比@import还有性能上的收益. @import rule会引起空白屏现象,甚至假如你在文档头里,正像下面例子中显示的那样.

CSS at the Top Using @import
http://stevesouders.com/hpws/css-top-import.php
使用@import rule引起一个问题,组件的下载顺序不可预期. 现在来看三个例子. 每个页面都含有8个http请求:One HTML page, Six images, One stylesheet. 这些组件在css-bottom.php和css-top.php页面中都是按照其出现的顺序下载的. 但是在css-top-import.php页面中,stylesheet最后被下载,因为他使用了@import rule. 结果是他出现了空白屏的问题,就像css-bottom.php这个页面.
……(略)
使用LINK tag把stylesheets放到文档的HEAD中. 那么为什么浏览器以这种方式工作呢?

Flash of Unstyled Content
空白屏现象是对于浏览器的行为的. 记住即使我们的stylesheet没有在页面渲染的时候使用.甚至当IE浏览器已经得到全部的必须组件,它仍旧会等待,直到那些不必须的 stylesheet全部下载完成后才开始渲染页面. 在页面文档中的stylesheet不影响下载时间,但是会影响渲染. David Hyatt已经解释了为什么浏览器会这么做:
If stylesheets are still loading, it is wasteful to construct the rendering tree, since you don’t want to paint anything at all until all stylesheets have been loaded and parsed. Otherwise you’ll run into a problem called FOUC (the flash of unstyled content problem), where you show content before it’s ready.
接下来的例子会示例这个问题.

CSS Flash of Unstyled Content
http://stevesouders.com/hpws/css-fouc.php
在这个例子中,文档从stylesheet中使用了一个CSS rules,但是这个stylesheet是放在底部的. 当页面在加载的进度中, text最先被显示, 接着是images. 最后,当stylesheet成功下载并解析后, 已经被显示的text和images被用新的style重新显示. 这就是在行为中的”flash of unstyled content”. 它应该是被避免的.
浏览器中的空白屏试图宽免那些犯错的前台后端工程师,他们把stylesheet放到了文档的后面. 空白屏阻止了FOUC问题的发生. 浏览器可以延迟渲染直到所有的stylesheet下载完毕, 但是引发了空白屏. 但是相对的,浏览器可以逐步的渲染最后重新渲染页面. 无论怎么做,这两者都是不完美的.

What’s a FrontEnd Engineer to Do?
那么如何避免空白屏和FOUC呢?
在”CSS Flash of Unstyled Content”的例子中,刷新不是永远发生的;他依赖于浏览器和你如何加载页面. 在早先的文中,我解释了空白屏只发生在IE浏览器上,当在新窗口中加载页面,或者刷新页面,或者作为首页. 在这些例子中,IE浏览器选择了空白屏的方式. 然后假如你点击一个链接,或者使用一个标签页,或者直接输入一个url,那么IE浏览器将选择第二种方式:FOUC.
David Hyatt,”Surfin’ Safari” blog, http://weblogs.mozillazine.org/hyatt/archives/2004_05.html#005496.
FireFox浏览器在这方面是一致的,它永远选择第二个方式(FOUC). 所有的例子在Firefox中的行为都是一致的: 逐步渲染. 对于首先的三个例子,Firefox工作的行为方式对用户是有意的,因为stylesheet对页面渲染是没有用的. 但是在”CSS Flash Unstyled Content”的例子中,用户是不幸的. 用户正好体验了FOUC问题因为firefox逐步渲染内容.
当浏览器的行为不一致时,前台后端工程师该怎么做么?
你可以在HTML规范文档中找到答案(http://www.w3.org/TR/html4/struct/links.html#h-12.3):
Unlike A, [LINK] may only appear in the HEAD section of a document, although it may appear any number of times.)
浏览器拥有支持惯例的历史,违反HTML规范是为了那些老的,不守规范的页面可以正常显示, 但是当他遇到处理stylesheet的位置时,IE和firefox浏览器都推动开发组织注意遵守规范. 违反规范的页面(在HEAD外放置LINK)仍旧被渲染,但是在一定程度上影响用户体验.

Put your stylesheets in the document HEAD using the LINK tag.

<high performance web sites> Gzip Components

June 21st, 2009 No comments

Rule 4: Gzip Components
一个HTTP从请求到响应并且穿越网络的时间可以减少,这是非常有意义的. 用户的带宽,网速;互联网提供商,接近对等交换点,等其他的一些因数都超出了开发团队的控制. 然而有很多变数影响响应的时间. Rule 1和 Rule 3忙于减少不必要的的http请求以达到响应时间的优化. 假设没有http的请求,那么也就没有网络行为–理想状态. Rule 2通过部署http响应更接近于用户,从而提高响应速度.
Rule 4,本章介绍的内容是通过减少http响应的数据量的大小以减少响应时间. 假如一个http请求的结果是一个很小的响应数据, 那么用于网络传输的时间也会减少,因为需要从服务器传输到客户端的数据包变少了. 这样的效果对那些带宽速度小的用户是巨大的. 本章将介绍如何使用Gzip编码来压缩HTTP的响应来达到减少网络响应时间. gzip是用户减少页面数据量的最简单的技术,而且有巨大的效果. 也有其他的方法可以减少HTML页面数据,比如删除注释,使用更短的url,但是这样做是典型的工作量大但收效少的活.

How Compression Works
文件压缩已经使用了数十年,比如在邮件里,在FTP站点. 从HTTP/1.1开始,客户端在http请求的head里使用Accept-Encoding注明了支持的压缩类型. 如: Accept-Encoding: gzip, deflate. 假如web服务器识别了请求头里的这个标志,那么它就可以使用客户端表明的压缩方法对响应数据进行压缩. 并且web服务器会通过在影响头里写上Content-Encoding来给客户端标识,比如: Content-Encoding: gzip. Gzip是目前广泛使用并且高效的压缩方法. 他是有GNU项目组根据RFC1952开发的. 唯一的其他一个压缩格式你可能会看到defalte,但是它在效率稍稍的弱一些,使用也并不广泛. 实际上,我只看到过一个网站使用defalte,它就是msn.com. 浏览器支持deflate,那么也会支持gzip,但是有些浏览器只支持gzip,却不支持deflate, 所以gzip是首选的压缩方式.

What to Compress
服务器基于文件类型选择文件来gzip, 在配置压缩中定义. 网站可以gzip他们的html文件. gzip脚本和css文件也是值得的,但是很多网站却错过这个优化. 实际上可以gzip所有的文本文件,包括xml和json. Image和PDF文件不需要使用gzip,因为他们已经是压缩格式,压缩他们就是浪费cpu资源, 也有可能使文件增大.
使用gzip的成本是: 服务器使用一部分的cpu时间来完成压缩,客户端解压缩这些压缩文件. 测定收益是否超过成本你不得不对响应数据量的大小,链接的带宽以及服务器与客户端的网络距离做综合考虑. 通常,给大于1或2k的文件做压缩是值得的. mod_gzip_minimum_size这个参数指定了被压缩的文件的最小值. 默认值是500 bytes.

The Savings
gzip通常减少响应数据量的70%. 见下:

     File type     Uncompressed size    Gzip size      Gzip savings    Deflate size    Deflate savings
     Script         3,277 bytes          1076 bytes     67%             1112 bytes      66%
     Script         39,713 bytes         14,488 bytes   64%             16,583 bytes    58%
     Stylesheet     968 bytes            426 bytes      56%             463 bytes       52%
     Stylesheet     14,122 bytes         3,748 bytes    73%             4,665 bytes     67%

以上数据清晰了表明了使用压缩后, gzip减少了66%的数据量, deflate减少了60%. 在这几个文件上,gzip优于deflate 6%左右.

Configuration
根据apache的不同版本,使用不同的压缩模块, apache 1.3使用 mod_gzip 而 apache 2.x使用mod_deflate. 接下来,我们看一下如果配置.

Apache 1.3: mod_gzip
在apache 1.3中的gzip压缩有mod_gzip模块提供. 有很多mod_gzip的定义指令,这在它的站点(http://www.schroepl.net/projekte/mod_gzip)上有描述. 下面介绍一下最常用的定制指令:

     mod_gzip on            
            //开启压缩模块功能
     mod_gzip_item_include
     mod_gzip_item_exclude   
            //指定那些需要压缩,那些不需要压缩, 根据文件类型,MIME类型,user agent等等指定

大多数的web主机服务已经默认在text/html上使用mod_gzip. 这是非常重要的配置变更可以明白的指定压缩scripts和css. 如下:

         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的使用率. 但是没有定义压缩等级的指令. 假如压缩带来Cpu的负载成为一个问题, 可以考虑缓存压缩的内容,在硬盘上或内存里. 压缩并且手工的维护缓存的工作可能成为一个负担. 幸运的是,有些选项可以自动在硬盘上保存压缩后的内容,并且当原source发生改变时自动的更新缓存. 可以使用 mod_gzip_can_negotiate 和 mod_gzip_update_static来完成.

Apache 2.X: mod_deflate
在apapche 2.x的版本中,可以使用mod_defalte模块来压缩. 尽管这个模块的名字是deflate,但是它是使用gzip来压缩的. 基本的压缩配置先前一节中的scripts和css可以在一行中完成,如:

     AddOutputFilterByType DEFLATE text/html text/css application/x-javascript

不象mod_gzip, mod_deflate包含了一个指定控制压缩等级: DeflateCompressionLevel. 更多的配置信息可以到apache网站查看: http://httpd.apache.org/docs/2.0/mod/mod_deflate.html

Proxy Caching
配置描述迄今为止都很好的工作. web服务器决定是否根据Accept-Encoding来压缩响应数据内容. 浏览器根据Expires或Cache-Control缓存响应数据,无论数据是否被压缩过.
当一个浏览器通过代理发送请求时,机制就会变得复杂. 假如第一个请求请求某个特定的url,从一个不支持gzip的浏览器来的. 这个请求到达proxy,并且proxy的缓存时空的. 接着proxy向web服务器转发请求. web服务器返回响应的内容,并且是没有压缩的. 无压缩的内容在proxy缓存,然后转发到浏览器. 接下来,假设另外一个请求,请求同样的url,并且发送这个请求的浏览器是支持压缩. proxy响应这个请求,并用缓存里的无压缩的内容返回给浏览器,忽略了使用gzip来优化. 假如两个请求的顺序对换,先请求的是支持压缩的,后请求的是不支持压缩的. 这个例子中,proxy拥有了一个压缩后的响应内容版本,然后他会把这个压缩内容返回给浏览器,不管它是否支持压缩.
解决这个问题的方法是web服务器在响应头里添加一个属性:Vary. web服务器告诉proxy根据请求的多样化缓存内容. 因为Accept-Encoding决定了压缩,所以感性的在Vary的属性里写上Accept-Encoding.如: Vary: Accept-Encoding
这样proxy就可以缓存多个版本的响应内容,每个Accept-Encoding对应一个版本. 先前的例子中,proxy就会缓存两个版本,一个压缩,一个无压缩. 在默认条件下,mod_gzip会添加Vary在响应头里. 关于Vary的更多信息请查看:http://www.w3.org/Protocols/rfc2616/rfc2616- sec14.html#sec14.44

Edge Cases
服务器与客户端之间的压缩看起来是简单的,但是它必须正确的工作. 页面会轻易的破损,假如服务器或者客户端之一犯了一个错误. 错误不会经常发生,但是下面的几个错误例子值得深思
现在接近90%的互联网流量传输到申明支持gzip的浏览器. 假如浏览器说它支持gzip,你必须相信它. 在IE的早期版本里有一些已知的bugs还未修复,特别是IE5.5 和IE6.0 SP1,微软已经发布了两片知识文档描述这个问题(http://support.microsoft.com/kb/313712/en-us and http://support.microsoft.com/kb/312496/en-us). 也有一些已知的其他问题,但是他们发生在特定的浏览器上,所在比例也不足互联网流量的1%. 一个安全的做法是让服务器在遇到已知可以压缩的浏览器的时候进行压缩,如:IE6.0或者最新版,Mozilla5.0或最新版. 这样的方式称为浏览器白名单.
在上述做法下,你可能错失一些压缩优化,有些浏览器是支持压缩的.二选一,用压缩的内容来服务不支持压缩的浏览器,这个显然更加槽糕. 在Apache1.3下浏览器白名单可以这样定义,如:

         mod_gzip_item_include reqheader "User-Agent: MSIE [6-9]"
         mod_gzip_item_include reqheader "User-Agent: Mozilla/[5-9]"

在apache2.x下可以如下定义:

         BrowserMatch ^MSIE [6-9] gzip
         BrowserMatch ^Mozilla/[5-9] gzip

在proxy上添加如bug的浏览器的缓存是相当复杂的. 不可能与proxy分享你的浏览器白名单配置. 在http头里指定可压缩的浏览器白名单太复杂. 最好的办法是在Vary属性里添加User-Agent作为另一个专为proxy的规范.例如:

         Vary: Accept-Encoding, User-Agent

mod_gzip会自动的在Vary里加入User-Agent这个信息,当它检测到你的配置里有使用浏览器白名单的时候. 不幸的是,有成千的的User-Agent. proxy缓存所有的Accept-Encoding和User-Agent组合的所有URLS的内容是不太可能的. mod_gzip的文档((http://www.schroepl.net/projekte/mod_gzip/cache.htm) 里说道”使用过滤规则评估浏览器头将导致所有的缓存失效”(原话:“using filter rules evaluating the User-Agent HTTP header will lead to totally disabling any caching for response packets). 因为这样事实上使得proxy缓存机制挫败. 另一种方式是屏蔽proxy得缓存机制,使用:Vary: * 或者 Cache-Contorl: private这样得http头信息. 头信息Vary:*防止浏览器使用缓存功能. 头信息Cache-Control:private是推荐使用,它使proxy针对所有浏览器得缓存机制失效. 当然这样做会增加你的带宽成本,因为proxy不会缓存你的内容.
如何平衡压缩与proxy支持的结论使复杂的,换来的是响应速度的提高,减少带宽成本,浏览器bug. 正确的答案根据你自己的网站做决定.
A: 假如你的站点的使用者非常少,或者只是小环境的用户(如:一个内网或者所有用户都使用FireFox 1.5),浏览器bug的问题就可以忽略. 使用压缩内容和Vary: Accept=Encoding. 这提高了用户体验,减小了页面个cmoponents的大小,利用了缓存代理.
B: 假如贷款成本紧凑,那么像上面的例子一样做. 减少了贷款成本,使更多请求有代理来处理.
C: 假如你拥有大量的,不同的用户,并且可以承担高的宽带成本, 拥有高质量的声誉,那么压缩内容并使用Cache-Control: Private. 这样禁止代理缓存,避免浏览器bug.
还有一个代理的问题需要指出.这个问题是,默认情况下,ETags(13章会描述)不会有任何表现无论内容是否已经压缩,所以代理可能会把错误的内容发送给浏览器. 这个问题在apache的bug里有描述(https://issues.apache.org/bugzilla/show_bug.cgi?id=39727). 最好的解决方法是禁用ETags. 这也是后面13章所建议的, 我会在那里做详细介绍.
Gzip in Action
这里有三个不同压缩程度的示范例子,你可以参考,在你的站点上使用.
Nothing Gzipped
http://stevesouders.com/hpws/nogzip.html
HTML Gzipped
http://stevesouders.com/hpws/gzip-html.html
Everything Gzipped
http://stevesouders.com/hpws/gzip-all.html
效果如下:

Example            (HTML,CSS,JS) size     TotalSize   SizeSave    ResponseTime   TimeSave
Nothing Gzipped    48.6K, 59.9K, 68.0K  177.6K   -                  1562 ms            -
HTML Gzipped       13.9K, 59.9K, 68.0K  141.9K  34.7K(19.7%)  1411 ms          151 ms (9.7%)
EverythingGzipped 13.9K, 14.4K, 18.0K  46.4K    130.2K(73.8%) 731 ms           831 ms (53.2%)

<high performance web sites> Add an Expires Header

June 11th, 2009 No comments

Rule 3: Add an Expires Header
快速响应时间不仅仅在设计web页面的时候考虑. 如果是这样的化,我们只要全部使用Rule 1,那么会变成一个极端的情况,没有images,scripts或者stylesheets在我们的页面里. 然而我们都知道,images,scripts和stylesheets可以提高用户体验,即使这意味着这个页面将要花更多的时间来加载. Rule 3,如本节所述,如何通过配置components使浏览器的缓存能力最大化,提高页面性能.
如今,页面包含了很多components,并且这个数量继续在成长. 首次访问你页面的用户,可能需要发出从多次http请求,但是通过使用http头里的未来Expires属性,你可以将那些components缓存. 这就避免在接下去浏览页面时的不需要的请求. http头里的未来Expires属性通常使用在images上,但其实应该使用在所有的components上,包括scripts,style和 flash. 大多数的顶级网站通常并没有这么做. 在这章里,我会指出那些站点原本可以更快,但又为什么没有那么快. 增加Expires这个属性会带来一些额外的开发成本,这在Revving Filenames小节中会描述.

expires Header
浏览器通过使用缓存减少了http请求的数量从而降低了服务器http响应,这样也是页面加载更加迅速. web服务器使用future Expires header通知web客户端拷贝当前的component直到一个特定的时间. 在HTTP规范里时这样概述的”the date/time after which the response is considered stale.” 它是HTTP响应时送出的.在http头里像这样的一个字符串”Expires: Thu, 15 Apr 2010 20:00:00 GMT”. 这样的一个未来过期的头告诉浏览器这个响应的内容不会变更或失效,直到2010年的4月15日. 假如这个信息是在请求一个页面的图片时返回的,那么浏览器会缓存这个图片,并在接下去访问的页面中使用,这样减少了http的请求次数.

Max-Age and mod_expires
在我介绍缓存提高性能如何好之前,提及一个二选一的Expires header是非常重要的. Cache-Control header在HTTP/1.1中被引入,解决了Expires header的缺点. 因为Expires Header使用一个特定时间,他需要服务器和客户端有严格的时钟同步. 而且过期时间必须被持续的检查,当未来的过期时间到来时,一个新的过期时间必须在服务器的配置文件里设定.
作为选择,Cache-Control使用max-age直接指定component能被缓存多长时间. 它定义了新的方式用秒为单位. 假如当前时间与上次返回时间差小于上次返回的max-age,那么浏览器使用缓存的版本,这样避免了额外的http请求. 如下一个例子,max-age设定10年:Cache-Control: max-age=315360000. 使用Cache-control解决了Expires的缺陷,但是你仍然希望为不支持HTTP/1.1的浏览器指定Expires header(虽然这部分的流量可能很少,不到总流量的1%). 你可以将Expires和Cache-Control max-age都放入响应头里. 假如他们两个都存在,HTTP规范规定,max-age的内容直接覆盖Expires header. 无论如何,假如你是尽责的,你始终会为时钟同步和修改服务器上过期设定而烦恼.
幸运的是,有一个apache的module “mod_expires”帮你像max-age一样设定Expires header. 可以通过”ExpiresDefault”指定,如:

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

这里指定的时间单位可以是年数,月数,星期数,天数,小时数,分钟数,甚至秒数. 它会在响应头里同时写上Expires和Cache-Control max-age.如:

         Expires: Sun, 16 Oct 2016 05:43:02 GMT
         Cache-Control: max-age=315360000

实际上,过期时间的值十分依赖与请求收到的时间,但是在这个例子中,它永远是10年过期. 自从Cache-Control占据主导作用并且用秒来表述和request的关系后,时钟不同步问题就可以被避免. 也没有了修改服务器过期时间的操作,并可以在HTTP/1.0的浏览器下工作. 最好的提升所有浏览器性能的方案就是使用ExpireDefault设定Expires header.

Empty Cache vs. Primed Cache
使用一个未来的expires header会影响pv数,当用户访问过你的网站后. 相反,当一个会员没第一次访问你的网站并且他的浏览器缓存是空的时候,它不会影响http请求的数量的.所以,对页面性能提升的影响取决与多久后用户带着原有的缓存访问你的页面的. 这是合适的,你的主要的流量是来自那些带着原有缓存的使用者. 把你的components做成可缓存的,为你使用者提交响应的速度.
这里说的”empty cache(无缓存)”,”primed cache(原有缓存)”,指的是浏览器与你的网站的缓存状态关系. 假如没有你的页面的components的缓存,那么称”空缓存”. 相反的,”原有缓存”指浏览器有你页面的可缓存的components缓存.
空缓存对比原有缓存的pv数量取决于网站应用的本质. 一个站点如”word of the day”,对于典型用户来说,基本上每个session只有一个pv. 这里有些问题,为什么”word of the day”这样的站点,它的用户在下次网文的时候,components不在缓存里呢?
1: 假如一个用户希望每次访问后可以看到更多的词,那么他会每周或每月来访问一次,而不是每天.
2: 用户上次访问后手工清除了缓存.
3: 用户访问了过多的网站,缓存被填满了,这样”word of the day”的components缓存就被浏览器清理了.
4: 浏览器关闭的时候,浏览器或者防病毒软件清除了缓存.
在一个session一个pv的情况下,”word of the day”的components在缓存里是不可靠的,所以它的页面使用原有缓存的百分比会低一些.
从另一方面来说,一个旅游或邮箱的站点,每个session带来多个pv,他的原有缓存的pv是可靠的并且是高百分比的. 这样的例子中,更多的pv将会在缓存中找到components.
我们观察了yahoo站点,发现:对于那些每天至少访问一次的不同用户用到缓存的比例是40%~60%,这依赖yhaoo! property. 同样的调查显示,使用缓存的pv数量百分比为75~85%.注意这里,第一个统计是根据用户而第二个是根据pv的. 我们可以看到使用原有缓存时,根据pv统计的百分比大于根据用户统计的百分比. 因为很多yahoo properties在一个session下收到多个页面浏览. 这份数据说明,增经没有任何缓存的浏览器访问,在同一时段的接下去的页面访问中都能用到前面访问后留下的缓存.
这些浏览器环境的统计显示优化原有缓存对用户体验是多么的重要. 我们想让40~60%的用户和75~85%的pv得到最优化. 这个百分比对你的网站来说是正常的,这个统计结果也是类似的,假如你的网站用户至少每月访问一次或者每次到你的网站都会访问多个页面. 通过使用未来expires header, 你增加了在浏览器缓存中components的数量,并且在接下去的页面流量中重用而不需要由网络再传输一个字节.

More Than Just Images
在图片上使用一个长时间不过期的头是如此的普遍,但是最好的实践是不要局限于图片. 一个长时间不过期的头也应该在任一不频繁变更的components上,如scripts,stylesheets和flash. 典型的,一个HTML的文件就不需要使用了,因为他包含了动态的内容,而且是根据每个用户展示不容内容. (我个人观点,这里也有例外了,有些static的页面还是可以cache的)
在理想的状态下,一个页面的所有components都应该被缓存,这样接下去的页面访问或许只需要一个http请求并返回纯html文件. 当所有的html文件中的compoents从浏览器缓存里读取的时候,响应的速度可以提高50%,甚至更多.

Revving Filenames
假如我们定制了浏览器和代理可以缓存我们的components,那么用户怎么能取到我们更新过的components呢? 浏览器一直使用本地缓存里的内容直到过期,它不会检查任何的修改直到过期. 这也是使用expires header减少请求响应的方式–直接从磁盘访问components而不产生HTTP流量. 这样,即使你更新了服务器上的components,用户浏览器也不会使用你的新版本.
为了确保用户能取得最新版本的compoents,我们必须修改所有HTML页面里的components的文件名. 这个修改可能是简单的,也有可能是痛苦的,这取决于你网站html页面的构建方式. 假如你使用PHP,Perl等动态的生成你的html页面,一个简单的方式是,在你引用component filaname的时候使用变量. 在这个前提下,变更你所有html页面里components文件名只需要在一个地方改变变量的定义就可以了.

Examples
http://stevesouders.com/hpws/expiresoff.php
http://stevesouders.com/hpws/expireson.php

<high performance web sites> Use a Content Delivery Network

June 9th, 2009 No comments

Rule 2: Use a Content Delivery Network
虽然用户的平均带宽每年都在增加,但是用户与web服务器之间始终受页面响应时间影响. 一个网站刚建立的时候,通常会把服务器放在一个地点. 假如这个网站成熟了,并且吸引了大量的用户群,那么它不得不面对一个问题,单一的服务器地点不再充分满足需求.它必须在其他地点成倍的部署服务器,物理上分散服务器.
作为实现物理分散内容的第一步,不要试图重新设计web应用到分布式架构下工作. 由于应用的依赖,重新设计包括一些麻烦的活,包括同步session状态,复制不同地点的db数据事务.
正确的做法是根据performance golden rule:一个页面,从第一个request开始到所有的reponse结束,花费在第一个request到reponse取得html document的时间,只占整个这个页面所有请求时间的10%~20%,而剩余的80%~90%时间花费在这个页面所包含的所有components的请求上.
假如web主应用服务器是接近用户的,那么响应一个http请求的时间是可以改善. 从另一个方面来说,假如一个页面的包含的components的web服务器接近用户的话,那么大多数的http请求响应时间将改善.相比之前为了分散应用而重新设计应用这样困难的工作来说,首先分散components应用是较好的办法. 这样做不仅达到了减少响应时间的效果,而且更加的简单,多谢content delivery network(内容分发网络).

Content Delivery Network
内容分发网络是一组跨越多个地点的分布式web服务器,使内容分发到用户端更加的高效. 这个高效使真对性能问题而言的,但是也能节省成本. 当性能最优化的时候,向特定用户分发内容的服务器是相对该用户在网络上最近,最优的服务器. 比如: 这个CDN可以选者一个离用户最近的或者响应速度最快的服务器.
一些大型的互联网公司拥有自己的CDN,相比使用CDN服务提供商的服务成本高效一些. Akamai Technologies公司是这个行业的领头羊.2005年该公司发布了Seppdera Networks,主要的降低了异地化的成本. Mirror Image Intenet公司,现在领先与Akamai. Limelight Networks是另一家公司.其他的供应商,像SAVVIS专注与视频内容的分发.
小型或非营利性的网站可能不能承担使用CDN服务的开销. 这里有一些提供免费CDN服务的组织,比如
Globule(http://www.globule.org)
CoralCDN(http://www.coralcdn.org)

他们部署不同的方式,一些需要终端用户修改浏览器使用proxy方式,也有可能是让使用该服务的网站开发人员修改components的urls使用不同的hostname.提防那些用http重定向用户请求到本地server的做法,这样会使页面变慢.
除了提高响应时间外,CDN带来其他的好处. 他们的服务包括了备份,存储容量扩展和缓存. CDN也可以帮助吸引忠实的用户流量,例如在天气或者财经新闻的高峰期,或者在的流行的体育或娱乐突发事件的时候.
依赖CDN的缺点是你的响应时间可能受到其他站点的影响,甚至包括你的竞争者. 一个CDN服务提供商被他所有的客户的使用者分享.另一个缺点是不能直接控制内容服务器. 比如,修改http response头信息只能通过服务提供商而不是自己的ops团队.最后是服务提供商的性能退化,所以还是要建立自己的CDN.
CDNs通常提供静态内容,像图片,脚本,css和flash. 提供动态页面包括特殊的主机要求:数据库链接,状态管理,验证,硬件或者操作系统优化等等,这些复杂的需求不包含在CDN提供的服务之内. 静态资源从另一方面来说是容易被部署且依赖少. 这就是CDN可以容易的为物理上分散的用户群大幅度提升响应时间的原因.

The Savings
两个在线的例子,使用CDN的在response time上获得了巨大的提升.两个例子都同样的包含5个script,一个stylesheet和8个图片.
详见steve souders的例子:
http://stevesouders.com/hpws/ex-cdn.php
http://stevesouders.com/hpws/ex-nocdn.php

<high performance web sites> Make Fewer Http Requests

June 8th, 2009 No comments

手上有本steve souders的<>的电子版,也不知道什么下载的,且是英文版的,所以一直搁在哪里,没有看.now工作性质发生了一点点变化,各方面相关知识都要熟悉一点了,一不小心找出了这本书,这次可要看完它啊.

这本书主要介绍如果提高网站响应性能,方法主要是优化静态资源这块,而非程序,框架层的优化.它总结了14条规则.

Rule 1: Make Fewer Http Requests(减少http请求次数)
根据作者先前追踪的网站性能分析结果,看可以看到一个页面,从第一个request开始到所有的reponse结束,花费在第一个request到 reponse取得html document的时间,只占整个这个页面所有请求时间的10%~20%,而剩余的80%~90%时间花费在这个页面所包含的所有components的请求上,所以减少页面所含元素的请求次数,可以大大提高页面渲染的速度. 有几种方法可以做到减少http request:

A: Image Maps
比如一个网站banner,有五个并排的小图片组成,每个图片带一个链接. 这样你就可以把这五个小图合成一张图片,然后用image map来实现原先的功能. 使用image map也有缺点,手工定义map会出现误差,除了四方型意外的其他形状比较难定义map,另外,使用DHTML定义的map在IE下无效.
具体代码如:

    <img usemap="#map1" border=0 src="/images/imagemap.gif">
    <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="settings.html" title="Settings">
      <area shape="rect" coords="141,0,171,31" href="help.html" title="Help">
    </map>

详见steve souders的例子: http://stevesouders.com/hpws/imagemap.php

B: CSS Sprites
css sprites也能够实现image map的整合图片的功能,而且相比更具有伸展性. 上面的例子可以改写为如下:

    <style>
    #navbar span {
    width:31px;
    height:31px;
    display:inline;
    float:left;
    background-image:url(/images/spritebg.gif);
    }
    .home { background-position:0 0; margin-right:4px; margin-left: 4px;}
    .gifts { background-position:-32px 0; margin-right:4px;}
    .cart { background-position:-64px 0; margin-right:4px;}
    .settings { background-position:-96px 0; margin-right:4px;}
    .help { background-position:-128px 0; margin-right:0px;}
    </style>
    <div id="navbar" style="background-color: #F4F5EB; border: 2px ridge #333; width:180px; height: 32px; padding: 4px 0 4px 0;">
    <a href="javascript:alert('Home')"><span class="home"></span></a>
    <a href="javascript:alert('Gifts')"><span class="gifts"></span></a>
    <a href="javascript:alert('Cart')"><span class="cart"></span></a>
    <a href="javascript:alert('Settings')"><span class="settings"></span></a>
    <a href="javascript:alert('Help')"><span class="help"></span></a>
    </div>

详见steve souders的例子: http://stevesouders.com/hpws/sprites.php

C: Inline Images
把图片元数据写入文本中,这样页面可以使用图片,但又不需要额外访问服务器. 可以用URL scheme来实现, 虽然这个功能ie是不支持的,但是如果使用对其他浏览器来说是值得的.
data:url方案在1995年首次提出,在rfc2397中,说:允许小数据量的元素使用,格式如下:
data:[][;base64],
一个文本直接使用的例子如下:

    <IMG ALT="Red Star" SRC="data:image/gif;base64,R0lGODlhDAAMALMLAPN8ffBiYvWWlvrKy/FvcPewsO9VVfajo+w6O/zl5estLv/8/AAAAAAAAAAAAAAAACH5BAEAAAsALAAAAAAMAAwAAAQzcElZyryTEHyTUgknHd9xGV+qKsYirKkwDYiKDBiatt2H1KBLQRFIJAIKywRgmhwAIlEEADs=">

详见steve souders的例子: http://stevesouders.com/hpws/inline-images.php
主要的缺点就是ie不支持,另外inline image的数据量长度限制,firefox 1.5开始,长度限制增到100k.
由于数据写死在页面中, 他们不能被cache,其他页面共享. 共用的图片可以写在css中,如:

    .home { background-image: url(data:image/gif;base64,R0lGODlhHwAfAPcAAAAAAIxKA...);}
    .gift { background-image: url(data:image/gif;base64,R0lGODlhHwAfAPcAAAAAAABCp...);}
    .cart { background-image: url(data:image/gif;base64,R0lGODlhHwAfAPcAAAAAADlCr...);}
    .settings { background-image: url(data:image/gif;base64,R0lGODlhHwAfAPcAAAAAA...);}
    .help { background-image: url(data:image/gif;base64,R0lGODlhHwAfAPcAAAAAALW1t...);}
    <div id="navbar" style="background-color: #F4F5EB; border: 2px ridge #333; width: 180px; height: 30px; padding: 4px 0 4px 0;">
    <a href="javascript:alert('Home')" title="Home"><span class="home"></span></a>
    <a href="javascript:alert('Gift')" title="Gift"><span class="gift"></span></a>
    <a href="javascript:alert('Cart')" title="Cart"><span class="cart"></span></a>
    <a href="javascript:alert('Settings')" title="Settings"><span class="settings"></span></a>
    <a href="javascript:alert('Help')" title="Help"><span class="help"></span></a>
    </div>

详见steve souders的例子: http://stevesouders.com/hpws/inline-css-images.php
inline images对于国内网站来说,基本上废了,国内用户基本上用的是ie

D: Combined Scripts and Stylesheets
整合页面的scripts到一个文件中,整个页面的stylesheets到一个文件中,有效减少request的数量.
这个盲目的合成一个文件是容易的,但是现在我们都是模块化开发,怎么整合文件,然后又要让开发方便? steve sounders给出的意见是在开发的时候保持独立modules,在编译,部署的时候把相应的modules整合到一个文件中.
单一的整合文件是简单的,但是又不能盲目的整合,这样会使整合后的文件数暴增. 10个modules文件可能整合出上千个单一文件. 也不要让的页面都load那些无用的modules. 如果一个网站有一打或者不同的modules整合体,那就要话点时间去分析页面,看你的整合是否合理.