一、内容篇
1. 尽量少的HTTP请求
80%的终端用户响应时间花在前端. 而这部分时间的大部分都用于下载页面的所有组件, 包括: 图片, CSS, 脚本, Flash等等 . 因此减少组件的数量可以减少加载这个页面时HTTP的请求数.这是加快页面的关键.
一种减少页面中的组件数量的方法是简化页面的设计. 但是有没有一种办法既能建立丰富内容页面, 又能满足快速度响应时间的要求. 这里有一些技术既可以支持丰富页面设计,又可以减少HTTP请求数.
合并文件是通过将所有脚本文件合并到单个脚本来降低HTTP请求数的办法。例如将所有CSS合并成单个CSS文件。当脚本和渲染文件每个页面都不相同时,聚合文件有更多的挑战, 但是完成这部分内容将改善你的响应时间.
CSS Sprites是减少图片请求数的首选方法.将你的背景图片合并到单个图片文件, 然后使用CSS的background-image和background-position 属性来显示所需的图像部分.
Image maps将多个图片聚合为单个图片. 整体尺寸大约一样, 但是减少了HTTP的请求次数, 从而加速了页面.Image maps只在图像是连续的时候才有用, 比如导航栏. 定义Image maps是乏味的且很容易出错. 使用Image maps制作的导航栏是不能使用的, 因此不推荐这种方法.
内联图片使用data: URL scheme在实际页面中嵌入图片数据. 这个会增加HTML文档的尺寸. 将内联图像聚合到你的渲染文件(被缓存)是一种减少HTTP请求数的办法, 这种办法可以避免增加页面的尺寸. 内联图像还没有被所有主流浏览器支持.
减少页面的HTTP请求数只是开始.对于第一次访问者来说这是最重要的改善性能的指导方针. 就像Tenni Theurer’s的博客文件 Browser Cache Usage – Exposed!所说的那样, 每天40%- 60%的访客访问你的网站时是没有缓存的. 让你的页面对第一次访问者快速是更佳的用户体验的关键.
2. 减少DNS查询
DNS系统将主机名映身为IP地址, 就像通讯录里的人名和电话号码之间的关系一样. 当你敲入www.yahoo.com到你的浏览器时, 浏览器会发一个DNS请求来获取您输入的主机名的IP. 这个过程一般将花费20-120ms, 在这个过程中浏览器不能从这个主机中下载任何数据, 直到DNS查询完成.
DNS查询可以缓存来获取更好的性能. 这种缓存可以由特定的缓存服务器上完成, 如用户的ISP或局域网, 但是也可以由用户各自的电脑完成. DNS信息保留在操作系统的DNS缓存( 在Windows系统中使用DNS Client服务) .大部分浏览器有与操作系统的DNS缓存分离的缓存. 只要浏览器在它自己的缓存中保持有DNS记录, 它就不会给操作系统发送获取记录的请求.
Internet Explorer默认的DNS查找缓存时间为30分钟, 在注册表中由DnsCacheTimeout指定.Firefox缓冲DNS请求1分钟, 由network.dnsCacheExpiration配 置设置(Fasterfox这个插件将这个时间改成了1小时).
当客户的DNS缓存为空的时候(操作系统和浏览器缓存都为空), DNS查找数就等于Web页面中单独的主机名的数量.这包括页面URL,图像,脚本文件, 样式表,Flash对象等的主机名.减少独立的主机名可以减少DNS查找的数量.
减少独立主机的数量就潜在的减少了在加载页面并行下载的数量. 避免DNS查找削减了响应时间,但是减少并行下载又可能增加响应时间. 我的指导方针就是将这些组件分离到两个主机但不超过4个主机. 这可以在减少DNS查找和允许并行下载的最大速度中获得一个折中方案.
3. 避免重定向
重定向通过301和302状态代码来完成. 下面是一个301响应的HTTP头部例子:
HTTP/1.1 301 Moved Permanently
Location: http://example.com/newuri
Content-Type: text/html
浏览器会自动将用户带到Location域指定的URL中. 重定向需要的所有信息都在头部. 响应的头部一般是空的. 虽然他们的名字, 在实际中301或302都不会被缓存,除非在头部指定了Expires或Cache-Control指示它应该被缓存. 元标签refresh和Javascript是引导用户到一个不同的URL的另外两种办法, 但是如果你必须做一个重定向, 优选的技术是使用标准的3xxHTTP状态代码, 主要是确保”后退”按钮能正常工作.
重定向需要记住的主要地方就是它会降低用户体验. 在用户和HTML文档中插入重定向会延迟页面的一切, 因为在HTML代码未到达前页面的任何组件都不会下载.
其中一个最浪费的重定向经常发生,Web开发人员一般都没有意识到这一点。它发生在一个URL应该有一个/,而URL丢失了这个正斜杠/. 比如访问http://astrology.yahoo.com/astrology将导致一个301响应, 这个响应包含一个到http://astrology.yahoo.com/astrology/的重定向. 在Apache中可以使用Alias或mod_rewrite或者如果你在使用Apache handlers的话,你可以用DirectorySlash属性来修复.
从一个旧站连接到一个新站是重定向的另一个常用地方. 另一些人在连接网站的不同部分然后根据用户的特定条件(如浏览器, 用户账户等)来重定向. 使用一个重定向来连接两个网站很简单,只需要很少的附加代码. 虽然在这些情况下使用重定向可以减少开发人员的复杂度, 但是它降低了用户体验. 如果两个代码路径是在同一个服务器,那么这种用途的重定向的替代方法是使用Alias和mod_write. 如果域名变化是使用重定向的原因, 一个替代的方法是在DNS中创建一个CNAME记录和使用Alias或mod_write这两个联合起来.
4.让Ajax可缓存
Ajax的一个专用好处在于Ajax提供了即时反馈给用户, 因为它异步地从后端Web服务器请求信息. 然而, 使用Ajax并不能保证用户不会在等待那些异步的Javascript和XML响应返回时无所事事. 在很多应用程序中, 用户是否会保持等待取决于Ajax是怎么用的. 比如在一个基于Web的Email客户端中, 用户会等待Ajax请求查找所有符合他们搜索标准的Email信息的返回结果. 要记住的很重要的一点是异步并不意味着即时.
为了提高性能, 优化这些Ajax响应是很重要的. 最重要的改善Ajax的性能的方法是让响应可以缓存, 这在 添加Expires或Cache-Control头中讨论.其它同样适用于Ajax的规则包括:
Gzip组件
减少DNS查询
最小化Javascript
避免重定向
配置ETags
让我们看一个例子吧. 一个Web2.0的email客户端可能使用Ajax去下载用户的通讯录来使用自动完成功能. 如果自用户上次使用email web应用程序时, 用户没有修改通讯录, 如果Ajax响应有缓存, 且这个缓存还没过期, 那么前面的通讯录响应就可以从缓存中读取.浏览器必须被通知什么时候使用前面缓存的通讯录, 什么时候要请求一个新的通讯录. 这个可以通过添加一个时间戳到通讯录的Ajax URL, 指示用户最后修改她的通讯录的时间,如&t=1190241612. 如果这个地址自从上次下载没有改变, 那时间戳就相同, 那通讯录可以从浏览器的缓存中读取, 这就可以减少一次额外的HTTP请求. 如果用户已经修改了通讯录, 时间戳就会确保新的URL不会匹配缓存的响应, 浏览器就会请求更新过的通讯录.
即使你的Ajax响应是动态创建的, 且只适用于单个用户, 但是他们仍可以缓存. 这样做可以让你的Web2.0的应用程序更快.
5. 延迟加载组件
你可以对你的页面进行更细致的查看,然后问问你自己:哪些内容是在页面最开始加载时就需要的? 其它内容和组件就是可以等待的.
Javascript就是分离装载前和装载后事件的理想方式. 举例来说你有用于执行拖曳和动画功能的Javascript代码和库, 这些就是可以等待的, 因为拖曳元素只能在初始加载完成之后才能进行. 其它一些延迟加载的候选方式包括隐藏内容(在一个用户动作完成之后出现的内容)和不可见区域的图片.
YUI Image Loader这个工具可以让你延迟加载不可见区域的图片,YUI Get utility是一个让JS和CSS动态加载的简单办法. 一个实际例子是在打开Firebug的网络面板的情况下查看Yahoo! Home Page .
当性能目标与其它Web开发最佳实践联系起来时非常好. 在这种情况下, 渐进增强的想法(progressive enhancement,一种Web前端开发策略)告诉我们, 如果支持Javascript, 使用Javascript可以改善用户体验, 但是你必须确保页面能在没有Javascript也能正常工作. 因此你在确保页面工作正常后, 你可以使用一些延迟加载脚本来增加你的页面.
6. 预加载组件
预加载好像与延迟加载相反, 但实际上他们有不同的目标. 通过预加载组件, 你可以利用浏览器的空闲时间来请求将来可能需要用到的组件如图像, 样式和脚本.通过这种方法在用户访问下一页时, 你可以把大部分内容预先放在缓存中, 这样你的页面对用户来说就会加载快很多.
有以下几种方式的预加载:
无条件的预加载 – 只要加载文件, 你就继续去取一些额外的组件. Google.com是一个例子. 在打开Google.com时, Google会同样发送一个sprite图像, 而这个图像在Google.com首页中用不到,但是在搜索结果页中需要用到.
有条件的加载 – 基于用户行为, 你做一个猜测, 用户下一步会做什么, 然后预加载相应的内容. 在search.yahoo.com中你可以看到当你开始在输入框中输入时一些额外的组件是如何被请求的.
基于预测的预加载 – 在发布一个重新设计之前预加载. 在重新设计之后,你经常听到: 新的网站很好, 但是它比之前的要慢. 这个问题的部分原因在于用户在访问旧的网站时用了所有的缓存, 而新网站的缓存是空的. 你可以通过在发起重新设计之前预加载组件来减轻这种副作用. 你的旧站可以使用浏览器的空闲时间来请求新站要用的图像和脚本.
7.减少DOM元件的数量
一个复杂的页面意味着更多的字节的下载和Javascript中更慢的DOM存取. 例如当你添加一个事件处理器时,遍历500或5000个DOM元素就会有差别.
大量的DOM元素是一个征兆,很多可以通过无需移除内容的页面标记来改善. 你是否为了布局目的在使用嵌套的table? 你是否在为了修复布局问题而使用更多的<div>标签? 也许还有一个更美好,语义更正确的方式做标记。
布局可以使用YUI CSS utilities: grids.css可以助你设置整体布局, foonts.css和reset.css可以帮助你去除浏览器的默认格式. 这是一个重新开始的机会, 想想你的标记, 例如使用<div>标记是因为它语义和语法需要, 而不是因为需要加载一个新行.
DOM元素的数量很容易测试, 在Firebug的终端中输入以下命令:
document.getElementsByTagName('*').length
看看你的DOM元素有多少? 检查其它有好的标记的相似页面. 例如Yahoo! Home Page是一个非常繁忙的页面, 但是它使用的元素(HTML标记)还是在700个以下.
8. 在多个域中分离组件
分离组件允许你获得最大化并行下载. 确保你使用不超过2-4个域, 因为DNS查找也需要代价.比如你可以把你的HTML和你的动态内容分开, 动态内容动在www.example.org, 静态内容分为两个域static1.example.com和static2.example.com.
要获取更多信息, 请查看 Tenni Theurer 和 Patty Chi写的”Maximizing Parallel Downloads in the Carpool Lane“.
9. 使用最小数量的iframes
iframe允许HTML文档插入到上层文档. 要有效的使用iframes就要理解他们是如何工作的, 这是非常重要的.
<iframe>优点:
有助于慢的第三方内容如微标和广告
安全沙盒
并行下载脚本
<iframe>缺点:
即使空白也代价昂贵
阻止页面加载
无语义
10. 避免404错误
HTTP请求是昂贵的, 因此生成一个HTTP请求然后得到一个没用的响应是完全没有必要的, 会降低用户体验而得不到任何好处.
一些网站有带帮助性的404页面” 你的意思是X” , 这对用户体验来说不错, 但是也浪费服务器资源(如数据库等等). 特别坏的是当链接到外部Javascript出错时返回的是404, 首先这个下载会阻止并行下载, 其次因为看起来像是Javascript代码,浏览器会尝试解析404响应body, 尝试在body里面找出一些有用的东西.
二、服务器篇
1. 使用CDN( Content Delivery Networks)
用户与你的服务器的邻近程度对响应时间有一定的影响.部署你的内容在多个地理位置分散的服务器上, 从用户角度来说, 会使页面加载速度加快, 但是你应该从哪开始呢?
实施地理上分散的内容的第一步, 不要试图去重新设计你的web应用程序来让它工作在一个分布式的架构下. 依赖于特定的应用程序, 改变架构可能包含令人生畏的任务,如同步session状态和在多个服务器位置之间复制数据库. 试图减少你的用户和你的内容之间的距离可能会被这种应用程序架构步骤所拖延,或者从来都通不过。
记住80%-90%的终端用户响应时间花在下载页面中的所有组件: 图片, 样式,脚本, Flash等等. 这是性能黄金法则.更好的方法是从分发你的静态内容开始, 而不是从重新设计应用程序架构这样的艰巨任何开始.这将不只完成一个大的响应时间的减少, 而且由于CDN变得更容易.
一个内容分发网络(CDN)是Web服务器的合集,这些服务器分布在多个位置, 这就让内容分发更有效. 被选择用来向特定用户分发内容的服务器通常是基于网络邻近程度.比如选择有最少网络跳数的服务器或最快的响应时间的服务器.
一些大型的互联网公司拥有他们自己的CDN, 但是使用CDN服务商的CDN性价比更高.对于刚起步的公司和私人Web站点, CDN 服务的费用可以会望而却步, 但是当你的目标客户群增长和成为更全球化时, 要完成较快的响应时间CDN是必要的.转换到CDN是一个相对简单的代码转换, 但是会戏剧性的改善你的网站速度.
2. 添加Expires和Cache-Control头
这个规则有两个方面:
对于静态组件: 通过设置Expires头为很远的将来,以实施”Never expire”(从来不更新)策略
对于动态组件: 使用一个合适的Cache-Control头来帮助浏览器进行有条件的请求
Web页面设计变得越来越丰富, 这就意味着在页面中有更多的脚本,样式, 图片, Flash. 第一次访问你的页面的访客可能需要生成多个HTTP请求, 但是通过使用Expires头,你可以让那些组件可以缓存. 这就避免了在接下来的页面查看时生成不必要的HTTP请求. Expires头部最经常用在图片, 但是他们应该用在所有组件上,包括脚本, 样式和Flash组件.
浏览器(和代理)使用缓存来减少HTTP的请求和尺寸, 以让Web页面加载更快. 一个Web服务器在HTTP响应中使用Expires头来告诉客户端一个组件可以缓存多久. 这是一个设置为将来的Expires头, 告诉浏览器这个响应不会失效直到2010年的April 15.
Expires: Thu, 15 Apr 2010 20:00:00 GMT
如果你的服务器是Apache, 使用ExpireDefault属性来设置相对于当前日期的过期日期. 下面这个例子设置Expires日期从请求时间加上10年之后的日期.
ExpiresDefault "access plus 10 years"
记住,如果你使用一个远的将来Expires头, 当你要改变组件时, 你要改变组件的文件名. 在Yahoo!我们经常让这一步成为建立程序的一部分: 一个版本数字嵌入到组件的文件名当中, 如yahoo_2.0.6.js.
使用一个远的将来的头部只影响已经访问过你的网站的用户的页面查看. 对于第一次访问你的站点,缓存为空的用户的HTTP请求数没有影响.因此性能改善的影响依赖于用户在访问时用缓存命中的频率.通过使用一个远的将来头部, 你增加了浏览器缓存的组件的数量和在后续页查看到中的重复使用率而不用发送一个字节的数据.
3. Gzip组件
HTTP请求和响应花在网络上的时间可以由前端工程师的决定大大减少. 虽然用户的带宽速度, 互联网提供商, 与邻近节点的交换点, 这些都在开发团队的控制之外, 但是还是有很多变量影响响应时间. 压缩就是通过降低HTTP响应的大小来降低响应时间.
从HTTP/1.1开始, Web客户端通过在HTTP请求头部的Accept-Encoding头来指示是否支持压缩:
Accept-Encoding: gzip, deflate
如果Web服务器在请求中看到这个头, 它就会使用客户端列出的方式之一压缩响应. Web服务器通过响应中的Centent-Encoding头通知web客户端:
Content-Encoding: gzip
Gzip是当前最流行和最有效的压缩方式. 它由GNU项目开发, 由RFC 1952标准化. 其它你可能看到的压缩格式是deflate, 它没有gzip那么有效和流行.
Gzipping通常降低响应尺寸的70%. 现在通过浏览器传输的Internet流量大约占90%,而这些浏览器都声称支持gzip. 如果你使用Apache, gzip模块配置依赖于你的版本: Apache 1.3使用mod_gzip, Apache 2.x使用mod_deflate.
浏览器和代理有一些已知的问题, 这些问题导致浏览器期望的和它接收到的压缩内容不一致.幸运的是, 这个边缘案例在随着旧浏览器的下降而减少.Apache模块通过自动地添加适当的Vary响应头来帮助解决这个问题.
服务器根据文件类型来选择gzip. 但是通常都限制了他们选择的压缩的内容. 大部分web站点gzip他们的HTML文档. 同时也值得gzip你的脚本和样式, 但是很多web网站丢失了这个机会. 实际上, 压缩任意的文件响应包括XML和JSON都是可以的. 图像和PDF文件不应该被gzip, 因为他们已经压缩过了, 试图压缩他们不只浪费CPU,也潜在的增加文件尺寸.
Gzipping尽量多的文件类型是一个减少页面重量和加速用户体验的简单办法.
4. 配置ETags
Entity tags(ETags)是一个web服务器和浏览器用来确定浏览器缓存中的组件是否与原始服务器中的相同的一种机制(entity(实体)是组件的另一个说法,如图片,脚本,样式等都是实体). ETags提供一种对实体的验证机制, 这种机制比last-modified日期更加灵活.一个ETag是标识一个组件的特殊版本的一个独特的字符串,唯一的格式约束就是字符串引用. 原始服务器使用ETag响应头来指定一个组件的ETag:
HTTP/1.1 200 OK
Last-Modified: Tue, 12 Dec 2006 03:03:59 GMT
ETag: "10c24bc-4ab-457e1c1f"
Content-Length: 12195
然后, 如果浏览器要验证这个组件, 它使用If-None-Match头将ETag回传给原始服务器. 如果ETags匹配, 一个304状态码就会返回来, 这样就减少了响应的尺寸.在这个例子中减少了 12195 字节.
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"
HTTP/1.1 304 Not Modified
ETags的问题在于他们通常使用一个属性来构建, 而这个属性对托管网站的特定服务器是唯一的.当一个浏览器从某个服务器获取原始组件, 然后再尝试向另一台不同的服务器验证这个组件, 这时ETag就不会匹配, 这种情况在网站使用一个服务器集群来处理请求时很常见. 默认情况下Apache和IIS都在ETag中嵌入日期, 通过这种办法大大降低了有多台服务器的网站在验证测试成功的失败率.
Apache 1.3和2.x的ETag格式是inode-size-timestamp. 虽然一个指定的文件会在多个服务器的相同目录下驻留, 这个文件有相同的文件尺寸, 权限和时间戳, 但是他的inode在每台服务器上是不同的.
IIS 5.0和IIS 6.0的ETags有类似的问题.IIS上的ETags格式是Filetimestamp:ChangeNumber. 一个ChangeNumber是一个计数器, 这个计数器用来跟踪IIS配置的变化. 在网站后面的所有IIS服务器有相同的ChangeNumber是不太可能的.
最后的结果是由Apache和IIS在不同的服务器上针对几乎相同组件创建的ETags不会匹配.如果ETags不匹配, 用户不会接收到ETag本来设计的小而快的304响应, 相反地他们会获得一个正常的200响应, 同时带着组件的所有数据. 如果你只在一个服务器上托管你的网站, 那么这不是问题. 但是如果你的网站放在多个服务器, 然后你又在使用Apache或IIS, ETag使用默认配置, 那你的用户就会获得慢一些的页面,你的服务器会有一个较高的负载, 你在消耗更大的带宽, 代理也不能有效的缓存你的内容. 即使你的组件有一个远的将来的Expires头, 在用户点击重载或刷新时一个条件性的Get请求还是会发生.
如你不想利用ETags提供的灵活的验证模型, 那最好把ETag也一起移除. Last-Modified验证基于组件的时间戳. 在响应和接下来的请求中移除ETag都会减少HTTP头的尺寸.这一篇Microsoft Support article 讲了如何移除ETags. 是Apache上, 移除ETags可以通过添加下列行到你的Apache配置文件中:
FileETag none
5. 尽早地刷新缓存
当用户请求一个页面时, 后端服务器要花200-500ms来将HTML页组合到一起. 在这段时间里, 浏览器在等待数据的到达, 它是空闲的. 在PHP里你有flush()函数.它允许你发送部分已经准备好的HTML响应到浏览器, 从而浏览器可以在你的后端还在忙于剩下的HTML页时开始获取组件. 这种好处可以在繁忙的后端或轻前端上看到.
一个考虑刷新的好地方是HEAD之后, 因为HTML头通常较容易产生, 它允许你包含CSS和Javascript文件, 让浏览器可以在后端还在处理时开始并行获取.
举例:
... <!-- css, js -->
</head>
<?php flush(); ?>
<body>
... <!-- content -->
Yahoo! search 率先进行了研究, 来进行了实际用户测试来证明使用这种技术的好处.
6. 对Ajax请求使用Get
Yahoo! Mail团队发现当使用XML HTTP请求时, POST在浏览器中实施分为两步: 首先发送头部, 然后发送数据. 因此在发送一个TCP包时最好使用GET( 除非你有大量的cookies).IE的URL最大长度是2K, 因此如果你发送超过2K的数据你可能不能使用GET.
一个有趣的副作用是不上传(posting)任何数据的POST请求其行为像GET. 基于HTTP specs, GET的意思是获取信息, 因此你只有在请求数据时使用GET是有意义的, 而相反的是发送数据来存储到服务器端.
7. 避免空的图片src地址
空的src字符串属性的图片比人们想像的出现次数多. 它以两种形式出现:
直接的HTML
<img src=”">
var img = new Image();
img.src = “”;
这两种格式都会导致相同的作用: 浏览器再发送一个请求到服务器:
Internet Explorer 发送一个请求到这个页面所在的目录
Safari and Chrome 发送一个请求到实际的页面本身
Firefox 3 和更早的版本 与Safari和Chrome的行为类似, 但是3.5版本解决了这个问题, 不会再发送一个请求.
Opera 在发现一个图片的空的src地址时不做任何事情
为什么这种行为很不好:
发送大量的非预期流量会削弱你的服务器, 特别是页面每天的浏览量成千上万时.
创建一个从来不会被看的页面, 浪费服务器的计算资源
可能破坏用户数据. 如果你通过cookies或其它方式跟踪请求数据, 你就有毁坏数据的可能性. 即使图片请求没有返回一张图片, 所有的头部都被浏览器阅读和接收, 包括所有cookies.当其它的响应被抛弃时, 这种破坏可能已经产生.
产生这种行为的根本原因在于浏览器进行的URI解析. 这个行为在RFC 3986 – Uniform Resource Identifiers中定义. 当URI中遇到一个空的字符串,它被当作相对URI, 然后根据5.2节定义的算法进行解析. 这个特殊的例子, 一个空字符串, 列在5.4节. Firefox,Safari和Chrome 都按照规范正确的解析一个空字符串, 但是Internet Explorer不正确的解析它, 明显地还在根据早期版本的规定RFC 2396 – Uniform Resource Identifiers (this was obsoleted by RFC 3986). 因此技术上来说, 浏览器在做他们本来设想要做的事情. 问题是在这里, 空字符串明显是无意的.
HTML5在4.8.2节添加了src标签属性的描述来让浏览器不要产生一个额外请求:
src属性必须出现, 且包含一个合理的URL, 引用一个非互动, 可选动画, 既非分页也非脚本的图片资源. 如果元素的基本URI与文档的地址相同, 那么src属性值必须不是空字符串.
希望浏览器在将来不会有这个问题. 不幸的是, 对<script src=”">和<link href=”">没有条文进行规定. 也许作出确保浏览器不会意外执行这种行为的调整还需要时间吧.
这条规则由Yahoo!’s JavaScript高手Nicolas C. Zakas启发.要查看更多信息, 请查看 “Empty image src can destroy your site“.
三、Cookie篇
1. 减少cookie尺寸
HTTP cookie的使用有几个原因, 比如身份验证和个性化.关于cookie的信息在web服务器和浏览器中通过HTTP头部互相交换.保持cookie尽量小来减小cookie对用户响应时间的影响是很重要的.
更多信息请查看由Tenni Theurer and Patty Chi写的“When the Cookie Crumbles” , 主要内容包括:
去掉不必要的cookies
保持cookie尽量小来减小cookie对用户响应时间的影响
设置一个合适的Expires日期. 一个较早的过期日期或没有过期日期会让cookie更快删除, 改善用户的响应时间
2. 为页面组件使用无cookie的域名(Cookie-free Domains)
当服务器对一个静态图片发送一个请求, 请求中把cookie一起发送过去, 这些cookies对服务器没有任何作用. 因此他们只是创造了网络流量.你应该确保静态组件在请求时不使用cookie, 使用cookie-free请求. 创建一个子域然后把所有你的静态组件存放在那里.
如果你的域是www.example.org, 你可以在static.example.org存放静态组件. 然而如果你已经针对顶级域example.org设置了cookie, 然后所有到static.example.org的请求都会包含cookie. 在这个案例中, 你可以买一个新域, 然后把你的静态组件托管在那里, 并保持那个域cookie-free. 如 Yahoo! 使用yimg.com, YouTube 使用ytimg.com, Amazon 使用images-amazon.com 等等.
另外一个托管静态组件到一个cookie-free域的好处是一些代理可能会拒绝缓存带有cookie的组件.与上面类似, 考虑到cookie的影响, 如果你想知道你是否应该对你的首页使用example.org 或 www.example.org. 省略www让你对*.example.org没有选择, 只能写入cookie, 因此为了性能原因, 最好使用www子域和写cookie到那个子域.
四、CSS篇
1. 将样式放在顶部
当在Yahoo!调查性能时,我们发现移动样式到文档的HEAD好像让页面加载更快.这是因为放置你的样式在HEAD会允许页面逐步呈现.
关注性能的前端工程师想页面逐步呈现; 就是说我们想让浏览器尽快显示内容. 这对于大量内容的页面和较慢联网速度的用户来说尤其重要. 给用户视觉反馈, 进度指示的重要性已经经过了很好的研究和记录。在我们的案例中, HTML页面就是进度指示器. 当浏览器逐步加载页面, 先是头,然后导航栏, 顶部LOGO等等. 所有的东西对等待页面的用户来说都是视觉反馈.这可以改善整体的用户体验.
将样式放在文档的底部的问题在于它在很多浏览器中禁止了页面的逐步呈现, 包括IE. 这些浏览器阻止渲染,以避免如果他们的风格改变重绘页面元素. 用户卡在一个空白页中.
HTML specification明确声明样式要包括在页面的HEAD部分:”Unlike A, [LINK] may only appear in the HEAD section of a document, although it may appear any number of times.” 没有替代方案, 空白屏幕或无样式内容的flash值得冒这个险.最佳的解决方案就是遵循HTML的规定, 将你的样式放在文档的HEAD加载.
2. 避免CSS表达式
CSS表达式是一种动态设置CSS属性的一种强大方式. 它们从IE 5就支持了, 但是从IE8开始被弃用.举个例子, 背景颜色可以使用CSS表达式设置为每小时交替变化一次:
background-color: expression( (new Date()).getHours()%2 ? "#B8D4FF" : "#F08A00" );
就像这里展示的,表达式方法接受Javascript表达式.CSS属性被设置为Javascript表达式计算的结果. 表达式方法被其它浏览器忽略, 因此这种设置属性的方法对IE有效.
表达式的问题在于他们计算的次数比大多数人预想的要频繁. 不只在页面在渲染时和调整尺寸时计算, 而且在页面滚动, 即使当用户在页面移动鼠标时这个表达式也会重新计算.
对CSS表达式添加一个计数器可以让我们跟踪什么时候和多久计算一个CSS表达式. 在页面中移动鼠标可以轻松创建超过10,000次计算.
减少CSS表达式计算次数的一种方法是使用一次性表达式, 当第一次计算这样的表达式时就会设置样式属性值为一个显式值,以代替CSS表达式. 如果样式属性必须在页面生命期中动态设置, 使用事件处理器来代替CSS表达式是一种可选的方法. 如果你必须使用CSS表达式, 记住他们可能会计算上千次, 影响页面的性能.
3. 使用<link>代替@import
之前的一条最佳实践声明为了实现逐步呈现功能CSS应该放在头部.
在IE中@import的行为与在页面底部使用 <link> 的行为一样, 因此最好别使用它.
4. 避免过滤器
IE专有的AlphaImageLoader筛选器旨在修复IE版本小于7的半透明真彩PNG的问题.这个筛选器的问题在于当图像下载时它会阻止渲染并冻结浏览器. 这也增加了内存消耗,它会应用在每个元素上面, 而不只是每幅图像, 因此问题被加倍了.
最好的方法是完全避免AlphaImageLoader和使用正常降级的PNG8图像进行代替,PNG8在IE中是没有问题的.如果你真的需要AlphaImageLoader, 那就使用带下划线的hack_filter, 别惩罚了你的IE 7+用户.
五、JavaScript篇
1. 将脚本放在底部
脚本产生的问题在于他们阻碍并行下载. HTTP/1.1 specification建议浏览器在每个主机上并行下载的组件数不超过两个. 如果你将图片放在多个主机上, 你可以获得超过两个的并行下载数. 但是当下载脚本时, 浏览器不会开始任何其它的下载, 即使它们在不同的主机上.
在一些情况不容易将脚本移动到底部. 比如如果脚本使用document.write来插入页面的部分内容, 它就不能移往页面更底处. 还有作用域问题. 在许多案例中, 有一些方法来解决这些情况.
常常会出现的另一种建议是使用递延脚本。DEFER属性指示脚本不包含document.write, 同时也作为线索告诉浏览器它可以继续渲染. 不幸的是Firefox不支持DEFER属性. 在IE中, 这个脚本会被延迟, 但也不是完全按照自己需要的方式进行. 如果一个脚本被延迟, 它可以移到页面的底部, 这就会使页面加载更快.
2. 让Javascript和CSS外部化
很多性能规则关注如何管理外部文件. 然而, 在进行这些考虑之前你应该问一个更基础的问题: Javascript和CSS文件应该包含在外部文件中还是在页面自身内联.
在实际中使用外部文件通常会产生更快的页面,因为Javascript和CSS文件会被浏览器缓存.内联在HTML文档的Javascript和CSS在HTML每次下载时都会下载. 这减少了HTTP请求数, 但是增加了HTML文档尺寸. 在另一方面, 如果Javascript和CSS由浏览器缓存在外部文件中, HTML文档的尺寸在没有增加HTTP请求的情况下会减少.
关键因素是外部Javascript和CSS组件被缓存的频率相对于HTML文档被请求的频率.这个因素虽然难以量化, 但是可以使用各种指标衡量.如果在每次对话中查看多个页面, 且你的页面重复使用相同的脚本和样式, 那么通过缓存的外部文件可以获取更多的好处.
很多web站点陷在这些指标中. 对于这些网站, 最好的解决方案就是把Javascript和CSS作为外部文件部署. 唯一的例外就是在首页优选内联.比如Yahoo!’s front page 和My Yahoo!. 对于每个对话中有比较少页面查看次数的首页,可以发现内联Javascript和CSS可以导致更快的终端用户响应时间.
首页通常是很多页面浏览的开始, 有一些技术可以充分利用内联来减少HTTP请求,并通过外部文件来获得缓存带来的性能提升. 其中的一个技术就是在首页内联Javascript和CSS, 但是在页面加载完成时下载外部文件. 接下来的页面就会使用已在浏览器缓存中的外部文件.
3. 微缩Javascript和CSS
微缩是通过从代码中移除不必要字符来减少它的尺寸, 以此来改善加载时间. 代码微缩了所有注释和不必要的空格字符串(空格,新行和tab键). 在Javascript中, 因为下载文件的尺寸减小了,所以改善了响应时间性能. 微缩Javascript的两个流行工具是JSMin和YUI Compressor. YUI compressor 同时也微缩CSS.
模糊化处理是另外一种应用于源代码的优化.它比微缩更复杂, 因此它更可能在模糊化步骤本身产生Bug. 在一个美国十个顶级网站的调查中, 微缩减小了21%的尺寸, 而模糊化则是25%. 虽然模糊化有一个更多的尺寸减少比例, 但是微缩Javascript冒很少的风险.
除了微缩脚本和样式文件, 内联<script> 和 <style> 块也可以被微缩. 即使你gzip了你的脚本和样式, 微缩他们仍然可以减少5%的尺寸或更多. 由于Javascript和CSS的使用增加, 因此你会从微缩你的代码中获得好处.
4. 移除重复的脚本
在一个页面中包含相同的脚本两次会损害性能. 这并不像你想像的那么不寻常. 一个美国前十个顶级网站的调查显示这些网站中有两个包含了相同的脚本.在单个Web页面重复包含一个脚本的两个主要坏处在于: 文件尺寸和脚本数量. 当它发生时, 它通过创建不必要的HTTP请求和浪费Javascript执行来损害性能.
不必要的HTTP请求发生在Internet Explorer, 而Firefox不会. 在IE中, 如果一个外部脚本包含了两次, 且不能缓存, 它就会在页面加载时创建两个HTTP请求.即使脚本可以缓存, 额外的HTTP请求发生在当用户重载这个页面时.
除了浪费HTTP请求之外, 在多次运行脚本时时间也浪费了. 这种冗余的Javascript执行在Firefox和IE上都会发生, 而不管脚本是否可以缓存.
一种避免相同脚本被包含两次的方法是在你的模板系统中实现一个脚本管理模块.在HTML页面中包含一个脚本的典型方法是使用SCRIPT标签:
<script type="text/javascript" src="menu_1.0.17.js"></script>
PHP中的方法是创建名为insertScript的函数。
<?php insertScript("menu.js") ?>
除了可以防止相同的脚本被多次插入外, 这种方法还可以处理脚本问题, 如依赖检查和给脚本文件名添加版本来支持远的将来Expires头.
5. 最小化DOM存取
在Javascript中存取DOM元素是缓慢的, 因为为了有更多的响应页面, 你应该:
缓存已经存取过的元素
更新节点”offline”,然后添加他们到树
避免使用Javascript固定布局
更多信息可以查看 YUI theatre中由 Julien Lecomte写的“High Performance Ajax Applications” .
6. 开发智能事件处理器
很多时候感觉页面响应很慢, 因为有太多的事件处理器被关联到了DOM树的不同元素上, 这些元素会频繁地执行. 这就是为什么使用event delegation是一个好方法.如果你在一个div中有10个按钮, 只有一个事件处理器关联到div封装器中, 而不是每个按钮有一个处理器. 事件冒泡式地向上传递, 因此你可以捕获这个事件然后找出它起源于哪个按钮.
为了开始对DOM树做一些处理,你也不需要等待onload事件.通常你所需要的就是你想存取的元素在树中. 你不需要等到直到所有图像都下载了. DOMContentLoaded是一个你可以考虑用来代替onload的事件.但是你必须等到它在所有浏览器中可用时才可以兼容所有浏览器。你可以使用YUI Event工具, 这个工具有onAvailable方法.
更多信息请参考YUI theatre中由Julien Lecomte写的 “High Performance Ajax Applications” .
六、图片篇
1. 优化图片
当设计人员创建web页面中的图片完成之后, 在将这些图片上传到你的web服务器之前你还可以尝试做一些事情:
你可以检查GIF文件, 查看他们在图像中是否使用与调色板尺寸对应的颜色数量.使用imagemagick,通过identify -verbose image.gif 可以轻松的检查. 当你看到一个图片使用4色, 在调色板中有256色时, 就说明还有改善的空间.
试着转换GIF文件到PNG文件,看看是否节省了尺寸. 通常不会有多大的节省. 开发人员经常犹豫是否使用PNG, 因为只有有限的浏览器支持. 但是这已经是过去的事了. 真正的问题是在真彩PNG图片中的alpha-transparency, 但是与前面一样, GIF也不是真彩并且不支持可变透明. 因此GIF可以做的事, 一个PNG8同样可以做(除了动画). 下面这个简单的imagemagick命令生成完全可以安全使用的PNG图片:
convert image.gif image.png
在所有PNG图片上运行 pngcrush 或其它PNG优化工具
pngcrush image.png -rem alla -reduce -brute result.png
在所有JPEG图片上运行jpegtran, 这个工具可以可以做一些无损操作如旋转, 也能够用来优化和删除图片中的注释和其它一些没有用的信息(如EXIF信息)
jpegtran -copy none -optimize -perfect src.jpg dest.jpg
2. 优化CSS Sprites
在sprite中水平排列图片相对于纵向会产生更小的文件
在sprite中合并相似的颜色可以帮助你保持低的颜色数量, 理想情况下低于256色, 因此可以放进PNG8中.
在sprite中图片尽量做到” 移动友好” ,并且不要在在图片之间留下大的间隙. 这不会对文件尺寸有很大影响,但是当用户端把图片解压成像素图时会有更少的内存消耗.100×100的图片是10,000个像素, 1000×1000的图片则有1百万个像素点
3. 不要在HTML中缩放图片
不要使用大于你需求的图片, 因为你可以在HTML中设置图片的宽度和高度.如果你需要
<img width="100" height="100" src="mycat.jpg" alt="My Cat" />
那么你的图片 (mycat.jpg)就应该是100x100px而不能由 500x500px 的图片缩小.
4. 让favicon.ico小而且可缓存
favicon.ico是一个驻留在你服务器根目录下的图片. 它是极其需要的, 因为即使你不关心它浏览器还是会请求它, 因此最好不要使用404 Not Found来响应. 同时因为它在相同的服务器上, 每次它被请求时cookie都会发送. 这个图片会干扰下载序列, 比如在IE上,当你在onload时请求一个额外的组件, favicon就会在这些额外组件之前下载.
因此要减轻favicon.ico的弊端, 那就要确保:
它很小, 最好低于1K
以你感觉舒服的方式设置Expires头(因为你不能在改变它时重命名它). 你可以安全地设置Expires头为几个月后的将来. 你可以检查你的当前favicon.ico的最后修改时间来做一个明智的决定.
Imagemagick 可以帮助你创建小的favicons.
七、移动篇
1. 保持组件低于25K
这个限制与iPhone不会缓存大于25K的组件有关. 注意这是未压缩的尺寸. 在这个地方微缩很重要, 因为单独gzip可能还不够.
更多信息参考Wayne Shea 和 Tenni Theurer写的”Performance Research, Part 5: iPhone Cacheability – Making it Stick” .
2. 打包组件成一个Multipart文档
打包组件成一个multipart文档就像一封包含附件的邮件, 它帮助你通过一个HTTP请求获取多个组件(记住HTTP请求是昂贵的). 当你使用这种技术时, 首先检查你的用户端是否支持它(iPhone不支持).