灏天阁

雅虎35条军规,你知道几条?

· Yin灏

在工作中,前端的性能优化是非常重要的,那我们该从何入手?可以遵循雅虎前端优化 35 条军规,虽然这些“军规”中一些的原则有些过时(比如 CSS Sprite,不会真的还有人在用雪碧图吧,不会吧不会吧 😲),但是仍然能为我们的前端优化指明方向。下面就来看看雅虎这 35 条军规。

一、图片优化

1. 优化图片

  • 检查 GIF 图片的调色板大小是否匹配图片颜色数;
  • 把 GIF 格式转换成 PNG 格式,看看是否节省空间;
  • 运行 pngcrush 或其它工具来压缩 png 格式的图片;
  • 运行 jpegtran 或其它工具来压缩 jpeg 格式的图片。

2. 优化 CSS Sprite

  • 把图片横向合并而不是纵向,因为横向图片文件更小;
  • 把颜色近似的图片合并到一张雪碧图,这样可以让颜色数更少,如果低于 256 色就可以用 png8 格式;
  • 对移动端友好,合并时图片间的间距不要太大。虽然这对图片大小影响不是太大,但这样做可以节省用户代理把图片解压成像素映射时消耗的内存。100×100 的图片是 1 万个像素,而 1000×1000 的图片就是 100 万个像素了。

3. 禁止在 HTML 中缩放图片

不要在 HTML 中缩放图片。不要因为可以设置图片的宽高就去用比需要的大得多的图片。如果需要 100px * 100px 的图片,那就不要用 500px * 500px 的。

4. 用小的且可以缓存的 favicon

  • favicon.ico 是放在服务器根目录的图片,浏览器也会自动请求它,所以最好不要给一个 404 Not Found 响应。而且只要在同一个服务器上,每次请求它时都会发送 cookie,此外这个图片还会干扰下载顺序,例如在 IE 中,当你在 onload 中请求额外组件时,将会先下载 favicon。
  • 所以为了缓解 favicon.ico 的缺点,应该确保:
    • 足够小,最好在 1K 以下;
    • favicon.ico 一般是不进行更换的,所以可以给它设置 Expires 头,而且可以安全地设置为几个月,避免每一次打开页面都需要去进行请求。

二、CSS 优化

5. 将 CSS 样式放在顶部

将样式表移到 <head> 里会让页面加载地更快。这是因为把样式表移到 <head> 里允许页面逐步渲染。 ​

我们希望浏览器尽早的去渲染获取到的内容,这对大页面和网速慢的用户很重要。给用户视觉反馈(比如进度指标)就非常重要,HTML 页面就是进度指标。当浏览器逐步加载页面头部,导航条,顶部 logo 等内容时,这些都是给等待页面的用户的视觉反馈,能够提高整体用户体验。

把样式表放在文档底部的问题是它阻止了许多浏览器的逐步渲染,包括 IE。这些浏览器阻止渲染来避免在样式更改时需要重绘页面元素。所以用户会卡在白屏

6. 避免使用 CSS 表达式

CSS 表达式是强大的(可能也是危险的)设置动态 CSS 属性的方法。CSS 表达式的问题是它们可能比预期计算的更频繁。它们不仅在页面载入和调整大小时重新计算,也在滚动页面甚至是用户在页面上移动鼠标时计算。比如在页面上移动鼠标可能轻易计算超过 10000 次。要避免 CSS 表达式计算太多次,可以在它第一次计算后替换成确切值,或者用事件处理函数而不是 CSS 表达式。

7. 选择 舍弃@import

上面提到了一个最佳实践:为了实现逐步渲染,CSS 应该放在顶部。在 IE 中用@import 与在底部用效果一样,所以最好不要用它。

8. 避免使用(IE)过滤器

IE 专有的AlphaImageLoader过滤器用于修复 IE7 以下版本的半透明真彩色 PNG 的问题。这个过滤器的问题是它阻止了渲染,并在图片下载时冻结了浏览器。另外它还引起内存消耗,并且它被应用到每个元素,而不是每个图片,所以会存在一大堆问题。最佳做法是放弃 AlphaImageLoader,而优雅地降级到用在 IE 中支持性很好的 PNG8 图片来代替。

三、Cookie 优化

HTTP Cookie 的使用有多种原因,比如授权和个性化。Cookie 的信息通过 http 头部在浏览器和服务器之间交换。重要的是保证 cookie 尽可能的小,以最小化对用户响应时间的影响。可以对 Cookie 做如下优化:

  • 消除不必要的 Cookie;
  • 尽可能减小 Cookie 的大小来降低响应时间;
  • 注意设置 Cookie 到合适的域名级别,以免影响其它子域;
  • 设置合适的 Expires 日期。更早的有效期或者 none 可以更快的删除 cookie,提高用户响应时间。

当浏览器请求静态图片并把 cookie 一起发送到服务器时,cookie 此时对服务器没什么用处。所以这些 cookie 只会增加无意义的网络流量。所以应该保证静态组件的请求是没有 cookie 的。可以创建一个子域名来托管所有静态组件。

比如,域名是www.example.org,可以把静态组件托管在static.example.org。不过,如果把cookie设置在顶级域名 example.org 下,这些 cookie 仍然会被传给 static.example.org。这种情况下,可以启用一个全新的域名来托管静态组件。

注意:因为 cookie 是可以跨二级域名的,所以如果设置的顶级域名是 example.org,那么 static.example.org 也是可以被访问到的,因此需要启用一个全新的域名。

另外一个用没有 cookie 的域名提供组件的好处是,某些代理可能会阻止缓存带 cookie 的静态组件请求。

四、服务端优化

11. 使用 CDN(内容分发网络)

用户接近服务器就会减少响应时间。把内容发布到多个地理上分散的服务器可以让页面加载更快。80-90%的终端用户响应时间花费在下载页面中的所有组件:图片、样式、脚本、falsh 等,这就是业绩黄金法则。最好先分散静态内容,而不是一开始就重新设计应用程序结构。这不仅能够大大减少响应时间,还更容易表现出 CDN 的功劳。 ​

内容分发网络(CDN)是一组分散在不同地理位置的 web 服务器,用来给用户更高效地发送内容。典型地,选择用来发送内容的服务器是基于网络距离的衡量标准的。

12. 添加 Expires 或者 Cache-Control 头部

这条规则有两个方面:

  • 对静态组件:通过设置 Expires 头部来实现永不过期策略。
  • 对动态组件:用合适的 Cache-Control 头部来帮助浏览器进行有条件性的请求。

网站设计越来越丰富,这意味着更多脚本,样式,图片等。第一次访问的用户可能需要发出多个请求,但使用 Expires 可以让这些组件被缓存。这避免了访问子页面时没必要的 http 请求。Expires 一般用在图片上,但它应该用在所有的组件上,包括脚本、样式等。

浏览器使用缓存来减少 HTTP 请求数和大小,加快页面加载。服务器使用 HTTP 响应的 Expires 头部来告诉客户端一个组件可以缓存多久。注意,如果设置了 Expires 头部,当组件更新后,必须更改文件名。

13. 传输时用 gzip 等压缩组件

HTTP 请求或响应的传输时间可以被显著减少。压缩可以通过减少 http 响应的大小来缩短响应时间。从 HTTP/1.1 开始,客户端通过 http 请求中的 Accept-Encoding 头部来提示支持的压缩:

Accept-Encoding: gzip, deflate

如果服务器看到这个头部,它就会选用列表中的某个方法压缩响应。服务器通过 Content-Encoding 头部来提示客户端:

Content-Encoding: gzip

gzip 一般可减小响应的 70%。尽可能去 gzip 更多(文本)类型的文件,这也是提升用户体验最简单的方法。。html,脚本,样式,xml 和 json 等都应该被 gzip,而图片,pdf 等不应该被 gzip,因为它们本身已被压缩过,gzip 它们只是浪费 cpu,甚至增加文件大小。

14. 配置 ETags

实体标记(Entity tags,简称 ETag)是服务器和浏览器之间判断浏览器缓存中某个组件是否匹配服务器端原组件的一种机制。实体就是组件:图片,脚本,样式等。ETag 被当作验证实体的比最后更改(last-modified)日期更高效的机制。一个 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 将不匹配。然而服务器集群是处理请求的通用解决方案。如果不能解决多服务器间的 ETag 匹配问题,那么删除 ETag 可能更好。

15. 尽早清空缓冲区

当用户请求一个页面,服务器一般要花 200-500ms 来渲染整个页面。这段时间,浏览器是空闲的(等待数据返回)。在 php 中,有个方法 flush() 允许传输部分准备好的 html 响应给浏览器。这样的话浏览器就可以开始下载组件,而同时后台可以继续生成页面剩下的部分。这种好处更多是在忙碌的后台或轻前端网站可以看到。

较理想的清空缓冲区的位置是 HEAD 后面,因为 HTML 的 HEAD 部分通常更容易生成,并且允许引入任何 CSS 和 JavaScript 文件,这样就可以让浏览器在后台还在处理的时候就开始并行获取组件。

16. 对 Ajax 用 GET 请求

当使用 XMLHttpRequest 时,POST 请求被浏览器实现为两步:首先发送头部,然后发送数据。所以最好使用 GET,仅用一个 TCP 包发送(除非 cookie 太多)。IE 的 url 长度限制是 2K。如果 POST 不提交任何数据,那它跟 GET 行为类似。但从语义上讲,获取数据应该用 GET,提交数据到服务器用 POST。

17. 避免图片 src 属性为空

空 src 属性的图片的行为有两种形式:

  • html 标签: <img src="">
  • js:var img = new Image(); img.src = "";

上面的两种形式都会造成同一种后果:浏览器会向服务器发送另一个请求。

发送大量的意料之外的流量,会削弱服务器,甚至可能会破坏用户数据。如果你在跟踪请求状态,通过 cookie 或其它,可能会破坏数据。即使 image 的请求不会返回图片,但所有的头部数据都被浏览器读取了,包括 cookie。即使剩下的响应体被丢弃,破坏可能已经发生。 这种行为的根源是 uri 解析发生在浏览器。

RFC 3986 定义了这种行为,空字符串被当作相对路径,Firefox, Safari, 和 Chrome 都正确解析,而 IE 错误。总之,浏览器解析空字符串为相对路径的行为被认为是符合预期的。

html5 在 4.8.2 添加了对标签 src 属性的描述,指导浏览器不要发出额外的请求。幸运的是将来浏览器不会有这个问题了(仅在图片上)。不幸的是,<script src=""><link href="">没有这样的规范。

五、JavaScript 优化

18. 把脚本放到底部

脚本会阻塞并行下载。HTTP/1.1 规范建议浏览器每个域名下不要并行下载超过 2 个组件。如果图片分散在不同服务器,那么就能并行下载多个图片。如果脚本正在下载,浏览器就不开始任何其它下载任务,即使是在不同主机名下的。

有些情况下,把脚本移动到底部并不简单。比如,脚本中用了 document.write 来插入内容,它就不能被移动到底部。另外有可能有作用域问题。但大多数情况,这些问题都有办法解决。

一个替代建议是使用异步脚本。defer 属性表明脚本不包含 document.write,并且提示浏览器可以继续渲染。不幸的是,Firefox 不支持 defer 属性。如果脚本能异步加载,那么也就可以把它移动到底部,这样可以大大加快网页运行速度。

这里要注意 JavaScript 是会阻塞浏览器运行的,所以脚本文件尽量放到页面的最下面。

19. 使用外部 JavaScript 和 CSS

JavaScript 和 CSS 是应该包含在外部文件还是内联在页面里?

实际上,使用外部文件一般可以让页面加载更快,因为 JS 和 CSS 文件会被浏览器缓存。内而联的 JavaScript 和 CSS 在每次 HTML 文档下载时都被下载。虽然内联减少了 http 请求,但增加了 HTML 文档大小。另一方面,如果 JavaScript 和 CSS 被缓存了,那么 HTML 文档可以减小大小而不增加 HTTP 请求。

核心因素就是 JavaScript 和 CSS 被缓存相对于 HTML 文档被请求的频率。如果网站用户每个会话打开了多个页面,许多页面重复使用相同的 JavaScript 和 CSS,那么有很大可能用外部 JS 和 CSS 更好。

许多网站用这些指标计算后在中间位置。对这些网站来说,最佳方案还是用外部 JS 和 CSS 文件。唯一例外是内联更被主页偏爱。主页每个会话可能只会打开少量甚至一个页面,这时候内联可能更快。

注意,需要根据实际业务来选择内联还是外联。(内联就是直接写在 HTML 文件的 js 或者 css,外联就是引入的 js 或者 css 文件)

20. 压缩 JS 和 CSS

压缩就是删除代码中不必要的字符来减小文件大小,从而提高加载速度。当代码压缩时,注释删除,不需要的空格(空白,换行,tab)也被删除。在 JavaScript 中这样做能够提高响应性能,因为要下载的文件变小了。两个最常用的 JavaScript 代码压缩工具是 JSMin 和 YUI Compressor,YUI compressor 还可以压缩 CSS。

混淆是对代码可选的优化。它比压缩更复杂,并且可能产生 bug。在对美国 Top10 网站的调查中,压缩可减小 21%,而混淆可以减小 25%。除了外部脚本和样式,内联的脚本和样式同样应该被压缩。

除了压缩外部脚本和样式,行内的

21. 删除重复的脚本

在页面中两次引入相同的脚本会降低性能。当确实引入重复脚本,会发出不必要的 http 请求和浪费 js 执行时间。 ​

IE 会产生不必要的 HTTP 请求,而 Firefox 不会。在 IE 中,如果一个不可缓存的外部脚本被页面引入了两次,它会在页面加载时产生两个 HTTP 请求。即使脚本是可缓存的,在用户重新加载页面时也会产生额外的 HTTP 请求。 ​

除了产生没有意义的 HTTP 请求之外,多次对脚本求值也会浪费时间。因为无论脚本是否可缓存,在 Firefox 和 IE 中都会执行冗余的 JavaScript 代码。 ​

避免不小心把相同脚本引入两次的一种方法就是在模版系统中实现脚本管理模块。典型的脚本引入方法就是在 HTML 页面中用 script 标签:

<script type="text/javascript" src="menu_1.0.17.js"></script>

22. 尽量减少 DOM 访问

用 JavaScript 访问 DOM 元素是很慢的,所以为了让页面反应更迅速,应该:

  • 缓存访问过的元素的引用;
  • 在 DOM 树外更新节点,然后添加到 DOM 树;
  • 避免用 JavaScript 修复布局问题;

注意,能用 CSS 解决的事情,就尽量不用 JS 操作 DOM,DOM 操作开销很大。

23. 用智能的事件处理器

有时候页面看起来响应速度比较慢,是因为绑定到不同元素的大量事件处理函数执行太多次。一种更好的解决方法就是使用事件委托。另外,不必等到 onload 事件来开始处理 DOM 树,使用DOMContentLoaded 会更快。大多时候需要的只是想访问的元素已在 DOM 树中,所以不必等到所有图片下载完。

注意:

  • onLoad是的在页面所有文件加载完成后执行;
  • DomContentLoad是 Dom 加载完成后执行,不必等待样式脚本和图片加载。

六、移动端优化

24. 保持组件小于 25K

这个限制是因为 iPhone 不能缓存大于 25K 的组件,注意这里指的是未压缩的大小。这就是为什么缩减内容本身也很重要,因为单纯的 gzip 可能不够。

25. 组件打包到一个复合文档中

将组件打包到复合文档就像带有附件的电子邮件,它可以帮助我们通过一个 HTTP 请求获取多个组件(请记住:HTTP 请求很昂贵)。使用此技术时,首先检查用户代理是否支持它( iPhone 就不支持)。

七、内容优化

26. 减少 HTTP 请求数

80%的终端用户响应时间都花在了前端上,:大部分用于下载组件 js/css/image/flash 等。减少组件数必然能够减少渲染页面所需的 http 请求数。这是让页面更快的关键。

减少组件数的一个方法就是简化页面设计。保持富内容的页面且能减少 http 请求,有以下几个技术:

  • Combined files (合并文件):通过把所有脚本放在一个文件中的方式来减少请求数的,当然,也可以合并所有的 CSS。如果各个页面的脚本和样式不一样的话,合并文件就是一项比较麻烦的工作了,但把这个作为站点发布过程的一部分确实可以提高响应时间。
  • CSS Sprites(雪碧图):雪碧图可以合并多个背景图片,通过 background-imagebackground-position 来显示不同部分。
  • Image maps (图片映射):可以把多张图片合并成单张图片,总大小是一样的,但减少了请求数并加速了页面加载。图片映射只有在图像在页面中连续的时候才有用,比如导航条。给 image map 设置坐标的过程既无聊又容易出错,用 image map 来做导航也不容易,所以不推荐用这种方式。
  • Inline images (内联图片):使用 data:url scheme 来内联图片,将内嵌图像组合到(缓存的)样式表中同样也是一种减少 HTTP 请求并避免增加页面大小的方法。

减少请求数是为第一次访问页面的用户提高性能的最重要的指导。注意,Inline images 这里是将矢量图标转换为 base64 编码,然后直接内嵌到 HTML 文件或者 CSS 文件当中,减少 http 请求。

27. 减少 DNS 查询

在浏览器地址栏输入网址,通过 DNS 查询得到网站真实 IP。DNS 查询被缓存来就可以提高性能。这种缓存可能发生在特定的缓存服务器(ISP/local area network 维护),或者用户的计算机。DNS 信息留存在操作系统 DNS 缓存中。大多浏览器有自己的缓存,独立于操作系统缓存。只要浏览器在自己的缓存里有某条 DNS 记录,它就不会向操作系统发 DNS 解析请求。IE 默认缓存 DNS 记录 30 分钟,FireFox 默认缓存 1 分钟。

当客户端的 DNS 缓存是空的,DNS 查找次数等于页面中的唯一域名数。减少 DNS 请求数可能会减少并行下载数。避免 DNS 查找减少响应时间,但减少并行下载数可能会增加响应时间。指导原则是组件可以分散在至少 2 个但不多于 4 个的不同域名。这是这两者的一个平衡点。

28. 避免重定向

重定向用 301 或 302 状态码来完成。一个 301 响应 http 头的例子:

HTTP/1.1 301 Moved Permanently
Location: http://example.com/newuri
Content-Type: text/html

浏览器自动跳转到 Location 指定的路径。重定向所需的所有信息都在 http 头部,所以 http 主体一般是空的。301 和 302 响应一般不会被缓存,除非有额外的头部信息,比如 Expires 或 Cache-Control 指定要缓存。meta 刷新标签或 JavaScript 也可以跳转,但如果真要跳转,3xx 跳转更好,主要是保证返回键可用。 ​

最重要的是重定向会降低用户体验。在用户和 HTML 文档之间插入重定向会延迟页面中的所有内容,因为页面中的任何内容都无法呈现,并且在 HTML 文档到达之前不会开始下载任何组件。 ​

最浪费的跳转之一发生在 url 尾部斜杠(/)缺失。比如[http://astrology.yahoo.com/astrology](http://astrology.yahoo.com/astrology)会 301 跳转到[http://astrology.yahoo.com/astrology/](http://astrology.yahoo.com/astrology/)。这可以被 Apache 等服务器修复,如果使用的是 Apache 处理程序,则可以使用 Alias,mod_rewrite 或DirectorySlash指令来取消不必要的重定向。 ​

重定向最常见的用途是把旧站点连接到新的站点,还可以连接同一站点的不同部分,针对用户的不同情况(浏览器类型,用户帐号类型等)做一些处理。用重定向来连接两个网站是最简单的,只需要少量的额外代码。虽然在这些时候使用重定向减少了开发人员的开发复杂度,但降低了用户体验。一种替代方案是用 Alias 和 mod_rewrite,前提是两个代码路径都在相同的服务器上。如果是因为域名变化而使用了重定向,就可以创建一条 CNAME(创建一个指向另一个域名的 DNS 记录作为别名)结合 Alias 或者 mod_rewrite 指令。

29. 让 Ajax 可缓存

使用 ajax 的好处是可以向用户提供很快的反馈,因为它是向后台异步请求数据。但是,这些异步请求不保证用户等待的时间——异步不意味着瞬时。提高 ajax 性能的最重要的方法是让响应被缓存,即在上面第 12 条中讨论的 Expires 。其它方法是:

  • gzip 组件
  • 减少 DNS 查找
  • 压缩 JS
  • 避免跳转
  • 设置 ETags

30. 延迟加载组件

我们需要考虑什么是页面初始化时所必须的。剩下的内容和组件就可以延迟加载。JavaScript 是理想的(延迟)候选者,可以切分到 onload 事件之前和之后。比如拖放的 js 库可以延迟,因为拖动必须在页面初始化之后。其它可延迟的包括隐藏的内容,折叠起来的图片等。

31. 预加载组件

预加载看起来与延迟加载是相反的,但它的确有不同的目标。通过预加载可以利用浏览器的空闲时间来请求将来会用到的组件。这样当用户访问下一个页面时,会有更多的组件已经在缓存中,这样会极大加快页面加载。预加载类型:

  • 无条件预加载:一旦 onload 触发,你立即获取另外的组件。比如谷歌会在主页这样加载搜索结果页面用到的雪碧图。
  • 有条件预加载:基于用户动作,推测用户下一步会去哪里并加载相应组件。
  • 预期的预加载:在发布重新设计的网站前提前加载。在旧网页预加载新网页的部分组件,那么切换到新网页时就不会是没有任何缓存了。

32. 减少 DOM 元素的数量

一个复杂的页面意味着更多的内容需要下载,以及更慢的 DOM 访问。比如在有 500 个 DOM 元素的页面添加事件处理就和有 5000 个 DOM 元素是有区别的。

如果页面 DOM 元素很多,那么意味着可能需要删除无用的内容和标签来优化。

33. 把组件分散到不同的域名

把组件分散到不同的域名允许你最大化并行下载数。但要确保只用不超过 2-4 个域,因为存在 DNS 查找的代价。例如,可以把 HTML 和动态内容部署在www.example.org,而把静态组件分离到static1.example.org和static2.example.org。

34. 尽量少用 iframe

iframe 允许 html 文档被插入到父文档里。

<iframe>优点:

  • 帮助解决缓慢的第三方内容的加载,如广告和标志;
  • 并行下载脚本;
  • 安全沙箱;

<iframe>缺点:

  • 即使空的也消耗(资源和时间);
  • 阻塞了页面的 onload;
  • 非语义化(标签);

35. 杜绝 404

http 请求是昂贵的,所以发出 http 请求但获得没用的响应(如 404)是完全不必要的,并且会降低用户体验。一些网站会有特别的 404 页面提高用户体验,但这仍然会浪费服务器资源。最坏的情况的是当链接指向外部 js 但却得到 404 结果,这样首先会占用并行下载数,其次浏览器可能会把 404 响应体当作 js 来解析,试图从里面找出可用的东西。

- Book Lists -