雅虎Best Practices for Speeding Up Your Web Site
背景
在阅览《软件测试52讲》时提到了雅虎的前端优化规则,因此这里做一下笔记;
概述
目前,业界普遍采用的前端测试方法,是雅虎(Yahoo)前端团队总结的7大类35条前端优化规则,你可以通过雅虎网站查看这些规则,以及对各规则的详细解读。
内容 Content
1. 更少的http 请求 make fewer http Requests
80%的终端响应时间花费在前端。更多的时间用于下载该页面的内容:图片,stylesheets,scripts,flash等等;减少组件的数量其实就是减少加载页面需要的http请求;
减少组件数量唯一的方法就是简化页面设计。但是有没有更好的方法不需要简化页面设计同时又能达到更快的响应时间呢?这里有一些技术可以减少http 请求同时支持丰富页面设计。
合并文件 通过把所有脚本合并成一个脚本来减少http 请求数量,同样的可以合并所有css文件到一个stylesheet。当脚本和样式表因页而异时合并文件更有挑战性,但是将此作为发布过程的一部分可以提高响应时间。
css Sprites (翻译为图片合并,但我总觉得这个翻译蛮奇怪)是减少图片请求数量的首选方法。把你的所有背景图片合并到一个图片并且使用css background-image 和 background-position 属性去展示所需的图像部分。
image map 合并多个图片到一个独立图片。整体size差不多,但是减少http请求数量会加快页面展示。页面中图片是相邻的情况下image map才会起到作用,比如导航栏。定义image map的坐标是容易出错的。但是用导航栏用image map没法访问,因此不推荐。
页面减少http请求是第一步。这是一个很重要的提高性能的方法,特别是对于第一次访问的用户来说他们没有缓存网站资源,如果访问速度快是一个好的用户体验。
2. 减少DNS查询
DNS指向IP地址,类似电话簿人名指向电话号码。当你输入www.yahoo.com时,,DNS查询将返回服务端IP地址。DNS耗费时间,通常来说耗费20-120毫秒去查询到IP地址。在查询到IP地址之前,浏览器什么事都做不了。
缓存DNS查询来获得更好的性能。DNS可以缓存在特殊的缓存服务器(用户ISP/当地网络),同时也可以缓存在用户个人电脑。
DNS信息存在操作系统DNS缓存(Windows:DNS Client service)。越来越多的浏览器拥有自己的缓存,独立于操作系统缓存。只要浏览器自己保存DNS记录在他们的缓存信息里,那就不需要再向操作系统请求信息。
IE默认缓存DNS记录30分钟,
3. 避免跳转
跳转用301或302状态码来实现。
浏览器自动跳转到location指定的url.所有必要的信息都在头部。body和response通常是空的。301或302响应都是不会被缓存的,除非他们有expires或Cache-Control头部表明他们需要缓存。如果你必须要跳转,建议用3xx HTTP请求来完成以保证返回键可以正常工作。
跳转会降低用户体验,在用户和html文档间插入跳转会延迟页面显示直到接收到html文档,因为在那之前页面没什么东西被加载和组件开始下载。
其中一个发生最频繁然而浏览器没有意识到的是url缺少“/”,比如 http://astrology.yahoo.com/astrology 被重定向到 http://astrology.yahoo.com/astrology/ ;在apache可以用Alias或者mod_rewrite,或者DirectorySlash指令。
把旧网址重定向到新网址是另一个常见的跳转。
4. 使用Ajax缓存
使用Ajax的好处的他提供快速的反馈,因为他是异步请求;然而使用Ajax不能保证用户不需要等待异步js和xml的返回。在很多应用用户是否等待取决于Ajax如何使用。
提高性能很重要的一点是优化Ajax响应,提高Ajax性能 最重要的方法是缓存响应,就像在 添加Expires 或者 Cache-Control 头部 优化规则中讨论的一样。
一些其他的规则也可以应用在Ajax:
- Gzip 组件
- 减少DNS查询
- 使js变小
- 避免跳转
- 配置ETags
让我们看下web 2.0 email 客户端可能使用Ajax自动下载用户电话簿(地址?)。如果这个用户从上次使用email web app 之后没有再改过他的电话簿(地址?),并且如果这个Ajax响应使用Expires或者Cache-Control缓存下来了,那么上一次缓存下来的电话簿就能够从缓存里读取。浏览器必须明智的去选择什么时候重新请求,什么时候从上一次的电话簿缓存读取。这个可以通过添加时间戳到电话簿Ajax URL记录上一次修改电话簿的时间。如果用户没有修改过电话簿,那么时间戳将是相同的.
即使你的Ajax响应是动态创建的,并且只应用于一个用户,他们也可以缓存。这样做会让你的web 2.0 apps更快。
5.延迟加载(Post-load)组件
你可以密切关注你的页面并且问问你自己:页面初始化什么是必须完全显示的?
那么剩下的内容和组件都可以等待。
js是最理想的延迟候选者,可以划分再onload事件之前和之后。比如说js代码用来拖拉和动画都可以延迟,因为拖拉必须在页面初始化之后。隐藏的组件和折叠的图片都可以是延迟加载的。
6.预加载组件
延迟加载和预加载看起来是相反的,但是预加载的目标和延迟加载是不一样的。通过预加载组件你可以充分利用浏览器闲置时间来请求即将使用的组件(比如图片,样式,脚本)。当用户浏览下一个页面时你可以从缓存里面获取更多的组件来使你的页面加载更快。
这里有三种预加载类型:
- 无条件预加载 - 一旦onload 触发,你立刻获取额外的组件。比如google.com 预加载连续搜索结果页面的雪碧图,而实际上雪碧图在google.com首页使不需要的。
- 有条件加载 - 基于用户动作你猜测用户下一步动作然后预加载响应组件。在search.yahoo.com你可以看到当你输入信息时一些组件被预先加载了。
- 预期预加载 - 在发布重新设计的网站之前预加载。当网站重新设计后你经常会听到 "这个新站点真酷,但是它比之前更慢了。" 其中一部分的原因时用户浏览旧网站时他的浏览器上有很多缓存但是在你的新网站他没有任何缓存。在你发布重新设计的网站前你可以通过预加载某些组件来降低这个影响。你的旧网站可以使用浏览器闲置事件来请求将要在你的新站点被使用的图片和脚本。
7.减少DOM 元素数量
一个复杂的页面意味着更多bytes需要下载并且意味着更慢的dom访问。比如在500 DOM 元素和5000 DOM 元素之间添加事件处理就不一样了。
8.跨域拆分组件
跨域组件允许你最大化并行下载。确保你使用的域名在2-4之间,因为DNS查询的副作用,最佳的域名在2-4个之间。比如说,你可以标记你的html和静态内容为www.example.org并且拆分组件到static1.example.org和static2.example.org
9.最小化iframes数量
iframes允许一个html document被插入到一个父级document.理解irames如何被使用更有效是很重要的。
优点:
- 帮助解决缓慢的第三方组件如广告
- 安全沙盒
- 并行下载脚本
缺点: - 即使空的也消耗(资源和时间)
- 阻塞了页面的onload
- 非语义化(标签)
10. 不要404
http请求是昂贵的,所以发起http请求但是获取404是完全没必要的 并且没有任何好处反而会降低用户体验。
有一些站点会提供有帮助的404,比如“你的意思是“x”吗 ”。虽然这个提高了用户体验但是仍然花费了服务器资源。有些不好的是当指向外部js代码时会导致404。
server
1. 使用CDN
用户越接近你的服务器响应越快。把你的内容分发到多个服务器,在地理上来说分散服务器从用户角度来看 会使你的页面加载的更快。但是该怎么做呢?
实现在地理上分散服务器的第一步,不要重构你的web应用为分布式体系架构。改变你的架构可能包含艰巨的任务如同步session状态,跨服务器复制数据库事务。
记住80%-90%的终端响应时间花费在下载页面内容。比起重新设计应用架构来说,分散静态内容更好。这不仅能让你的响应时间更快而且还更容易。
一些大型的internet公司有他们自己的CDN,但是更划算的是使用CDN服务提供商。
2. 添加Expires 或者 Cache-Control 头部
两个方面:
- 静态资源: 设置永不过期头部(Expires header)来实现永不过期策略;
- 动态资源: 通过设置Cache-Control头部来帮助浏览器有条件的请求。
web页面设计越来越丰富,意味着越来越多的脚本,stylesheets,图片和flash。第一次访问的用户将会有很多http请求,通过使用expires头部可以缓存哪些组件。这可以避免在子页面发起不必要的http请求。expires头部,通常用在images,但是他们也应该在脚本,stylesheet,flash组件使用。
浏览器(代理)使用缓存减少http请求的数量和size来使web页面更快加载。web服务响应用expires头部告诉客户端某个组件可以被缓存多久时间。
Expires: Thu, 15 Apr 2010 20:00:00 GMT
如果你的服务端使apache,可以使用ExpireDefault指令设置expires头部,这个过期时间是相对于当前日期的。如ExpiresDefault "access plus 10 years"
请记住,当你使用far future expires头部时,每当你的组件改变时请同时变更你的组件文件名。在雅虎,我们通常在构建步骤做这个事情:一个版本号将会被附加到组件文件名,比如yahoo_2.0.6.js
只有当老用户浏览你的网站时,expires头部才能发挥作用。如果是新用户或者浏览器缓存被清空的情况下expires头部对减少你的http请求数量是没有影响的。
3. 压缩(gzip)组件
转换成http请求和获取响应花费的时间明显减少,这取决于前端工程师。终端用户带宽,ISP,接近对等交换点这些开发团队没办法控制。 压缩组件明显减少HTTP响应size.
HTTP1.1,WEB客户端表明通过Accept-Encoding请求头部支持压缩
Accept-Encoding: gzip, deflate
而web服务器通过这个头部通知客户端
Content-Encoding: gzip
gzip是目前最流行和有效的压缩方法。另一个你可以知道的压缩格式是deflate,但是它没有那么有效和流行。
gzip通常可以减少70%的响应size.
众所周知,xml,json,image,pdf不应该被gzip,因为他们已经是被压缩过的。如果再次gzip不仅占用cpu而且可能还会增加文件大小。
4.配置ETags
ETags是一种机制,他是浏览器和服务端用于确定浏览器缓存组件是否在远程服务器找到匹配的实体。(一个entity另一个说法是组件:images,scripts,stylesheets等等);ETag是一种用来验证entity比上一个版本更加灵活的机制。一个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记录下来。如果ETag匹配,304状态码将会返回减少响应值。
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
ETag有一个问题是它通常是使用属性构造使组件对服务端来说是唯一值。当一个浏览器从一个服务端获取原始组件并且后面尝试在另一个服务端验证组件ETag无法匹配。一个常见的场景就是集群服务器。Apache和IIS通过嵌入ETag数据来减少在多个服务器网站上成功进行有效性测试的几率。
Apache 1.3 和2.x 是inode-size-timestamp,虽然他们在多个服务器之间在同一个目录下存放同一个文件,并且拥有相同文件大小,权限,时间戳等等,但是他们拥有不同的索引节点。
IIS5.0和6.0有一个关于ETag的issue.ETag的格式是Filetimestamp:ChangeNumber; 一个ChangeNumber是被用来跟踪配置变化的计数器。因此在所有的IIS服务器ChangeNumber不太可能是同一个。
结果就是 IIS和Apache标识同一个组件构造的ETag和另一个不一样。如果ETag不匹配,那么用户就不能获得最小最快的304响应。相反的特们将会得到一个正常的200响应获得组件的所有数据。如果你只有一个服务器那么这不会有问题。但是如果你有多个服务器并且你同时使用Apache或IIS并且使用默认ETag配置,你的网站将会更慢,你的服务器将会更高负载,你将会花费更大的带宽,并且代理不会有效缓存你的内容。即使你的组件拥有永不过期头部Expires.
如果你不能利用ETags灵活的验证模型,那么你最好把它完全移除。Last-Modified头部验证是基于组件时间戳的。并且移除ETag可以同时减少响应和请求的HTTP头部大小。
名词解释
- 集群结构
百度百科:服务器集群就是指将很多服务器集中起来一起进行同一种服务,在客户端看来就像是只有一个服务器。 集群可以利用多个计算机进行并行计算从而获得很高的计算速度,也可以用多个计算机做备份,从而使得任何一个机器坏了整个系统还是能正常运行。
分布式与集群的区别是什么?-大闲人柴毛毛
相当于多个单机 形成一个集群,那怎么知道用户请求由哪个节点处理呢? 这时候就需要一个“调度者”-负载均衡服务器来处理。
集群结构的好处就是系统扩展非常容易。如果随着你们系统业务的发展,当前的系统又支撑不住了,那么给这个集群再增加节点就行了。但是,当你的业务发展到一定程度的时候,你会发现一个问题——无论怎么增加节点,貌似整个集群性能的提升效果并不明显了。这时候,你就需要使用微服务结构了。
2.分布式结构
即微服务架构。分布式结构就是将一个完整的系统,按照业务功能,拆分成一个个独立的子系统,在分布式结构中,每个子系统就被称为“服务”。这些子系统能够独立运行在web容器中,它们之间通过RPC方式通信。
5.提前刷新缓冲区(Flush the Buffer Early)
用户打开一个页面通常需要花费200-500ms从服务端获取HTML页面资源。在这个过程浏览器闲置直到数据返回。而在PHP你可以使用flush()
发送部分html响应给客户端,客户端可以抓取组件当你的后端忙于页面的其余部分。好处是轻前端重后端。
一个好地方可以考虑使用缓存是在HEAD之后,因为head总是被最早渲染并且它允许你包含CSS和JS文件并行抓取尽管后端仍然在processing。
6. Ajax请求使用GET
当使用XMLHttpRequets时POST在浏览器实现有两个步骤: 发送headers - 发送data.所以使用 GET是最好的。因为GET只有一个TCP包裹要发送除非你有很多cookies.IE能发送的URL长度是2K,因此如果你的数据大于2K请不要使用GET。
7.避免空image src
image src可能出现的情况:
html
<img src="">
js
var img = new Image();
img.src = "";
导致一个问题: 浏览器会额外发一个请求给你的服务器.
- Internet Explorer makes a request to the directory in which the page is located.
- Safari and Chrome make a request to the actual page itself.
- Firefox 3 and earlier versions behave the same as Safari and Chrome, but version 3.5 addressed this issue[bug 444931] and no longer sends a request.
- Opera does not do anything when an empty image src is encountered.
为什么这是个不好的行为?
- 给服务器造成大量不需要的通信量
- 服务器循环构造这些根本不会展现的资源
- 可能损坏用户数据。如果你正在跟踪请求状态,或者通过另一个方法获取cookie,你有可能损坏数据。即使这个imgae请求没有返回image,所有的headers被浏览器读取和接收,包括cookies.尽管剩余的响应被扔掉,损坏也造成了。
导致这个行为的原因是URI解析在浏览器中执行。详见RFC 3986.
HTML5定义了src tag 属性,不会再额外发送一个请求。
然而像<script src="">
and ·<link href="">.
并没有相关的规定。
cookie
减少cookie size
HTTP cookies被使用的原因有很多,比如认证和个性化。在服务端和浏览器间使用headers来交换cookies信息。保证cookies的size越小越能降低对用户响应时间的影响。
- 消除不必要的cookies
- 保证cookies的size越小越能降低对用户响应时间的影响。
- 适当的domain设置cookies ,对子domain没有影响
- 设置适当的过期时间,一个过早的过期时间或没有过期时间很快把cookies移除,提高用户响应时间。
使用无cookie域名组件(Use Cookie-free Domains for Components)
- 你可以创建子域名来存放 静态资源,并且没有设置cookies;
- 采取第一种方式时,如果主域名含有cookie,那么也会被传给子域名。因此你可以买一个全新的域名来存放 静态资源,并且保证没有设置cookies;
存放无cookies域名 静态组件的好处是某些代理可能会拒绝缓存哪些带有cookies请求的组件。
css
1. 把样式放在顶部
我们发现把样式放在HEAD可以使页面更快加载。这是因为把样式放在HEAD允许页面慢慢加载。
关系性能的前端工程师希望页面慢慢加载。我们想要浏览器尽快展示无论他的内容包含什么。这点对哪些包含很多内容的页面和网速慢得用户来说特别重要。给用户视觉反馈是很重要的,比如进度条。
把样式放在document底部的问题就是很多浏览器禁止加载,包括IE。
2. 避免CSS表达式
css表达式就是在css属性后使用expression()连接一段JavaScript表达式,css属性的值是JavaScript表达式的结果。
background-color: expression( (new Date()).getHours()%2 ? "#B8D4FF" : "#F08A00" );
CSS表达式的问题就是计算过于频繁。不止加载页面,同时滚动页面,移动鼠标都可能在计算。
3. 选择 而不是@import
在IE @import
和在底部使用<link>
的效果是一样的,所以最好不要使用@import
4. 避免IE过滤器
IE的 AlphaImageLoader过滤 目标是修复半透明真彩色PNG在版本7以下的一个问题,当图片被下载时过滤器中断加载并且冻结浏览器。同时增加内存占用并且应用于每一个元素而不是每一个图片,问题成倍增加。
最好的方法是完全避免 AlphaImageLoader并且使用PNG8。如果你需要使用AlphaImageLoader那请你使用underscore hack _filter(IE7+)。
JavaScript
把脚本放在底部
脚本的问题是阻塞并行下载。HTTP 1.1规定建议浏览器每一个域名并行下载不能大于2个组件。然而当脚本在下载时,浏览器不会开始下载其他组件,即使在不同域名;
在某些 场景把脚本移到底部是不容易的。比如说脚本使用document.write去插入页面内容这个不能被移到页面底下。
另一个建议是使用递延脚本。DEFER属性表明这个脚本不包含document.write。
在这里我又去查了下defer到底是什么鬼。翻到这篇文章:浅谈script标签的defer和async。defer是先下载延后加载,并且最好只有一个defer属性。
使用外部JS和CSS文件
使用外部文件可以更快产生页面因为JS和CSS被浏览器缓存。如果放在HTML document 每次请求都要下载。这个能减少HTTP请求,但是增加HTML document 大小。另一方面,如果JS和CSS作为外部文件被浏览器缓存,那么HTML document 大小被减少而没有增加HTTP请求。
压缩(Minify)JS和CSS
移除不必要的元素来减少大小从而减少加载时间。两个流行的工具是JSMin和YUI Compressor.YUI Compressor也可以用来压缩CSS。当代码压缩时,注释删除,不需要的空格(空白,换行,tab)也被删除。
混淆是另一个优化代码的方法。它比压缩更复杂,因此使用它更同意产生BUG。
不仅外部script和style被压缩,内部