减少 HTTP 请求数

Web 前端 80% 的响应时间花在图片、样式、脚本等资源下载上。最直接的方式是减少页面所需资源,但并不现实。所以,减少 HTTP 请求数主要的途径是:
合并 JS/CSS 文件
服务器端(CDN)自动合并,基于 Node.js 的文件合并工具,通过把所有脚本放在一个文件中的方式来减少请求数。
最好的优化是做到页面只加载一个 CSS 和一个 JS。
使用 CSS Sprite
将背景图片合并成一个文件,通过 background-image 和 background-position 控制显示
行内图片(Base64 编码)
使用 Data URI scheme 将图片嵌入 HTML 或者 CSS 中;或者将 CSS、JS、图片直接嵌入 HTML 中,会增加文件大小,也可能产生浏览器兼容及其他性能问题。
减少页面的 HTTP 请求数是个起点,这是提升站点首次访问速度的重要指导原则。

使用 CDN(内容分发网络)

用户与服务器的物理距离对响应时间也有影响。把内容部署在多个地理位置分散的服务器上能让用户更快地载入页面。但具体要怎么做呢?
实现内容在地理位置上分散的第一步是:不要尝试去重新设计你的 Web 应用程序来适应分布式结构。这取决于应用程序,改变结构可能包括一些让人望而生畏的任务,比如同步会话状态和跨服务器复制数据库等事务。缩短用户和内容之间距离的提议可能被推迟,或者根本不可能通过,就是因为这个难题。
网站 80-90% 响应时间消耗在资源下载上,减少资源下载时间是性能优化的黄金法则。相比分布式架构的复杂和巨大投入,静态内容分发网络(CDN)可以以较低的投入,获得加载速度有效提升。
内容分发网络(CDN)是一组分散在不同地理位置的 web 服务器,用来给用户更高效地发送内容。典型地,选择用来发送内容的服务器是基于网络距离的衡量标准的。例如:选跳数(hop)最少的或者响应时间最快的服务器。

Expires 或 Cache-Control 响应头

这条规则有两个方面:

  • 对于静态组件:通过设置一个遥远的将来时间作为 Expires 来实现永不失效
  • 多余动态组件:用合适的 Cache-Control HTTP 头来让浏览器进行条件性的请求

网页设计越来越丰富,这意味着页面里有更多的脚本,图片。站点的新访客可能还是不得不提交几个 HTTP 请求,但通过使用有效期能让组件变得可缓存,这避免了在接下来的浏览过程中不必要的 HTTP 请求。有效期 HTTP 头通常被用在图片上,但它们也应该被用在所有组件上,包括脚本、样式等静态文件中。
一般来讲,浏览器等客户端都会有缓存机制来减少 HTTP 请求的数目和大小,让页面能够更快加载。Web 服务器通过有效期 HTTP 响应头来告诉客户端,页面的各个组件应该被缓存多久。用一个遥远的将来时间做有效期,告诉浏览器这个响应在 2022 年 4 月 15 日前不会改变。
如果你用的是 Apache 服务器,用 ExpiresDefault 指令来设置相对于当前日期的有效期。下面的例子设置了从请求时间起 10 年的有效期:

ExpiresDefault "access plus 10 years"

因为 Expires 头使用一个特定的时间,它要求服务端和客户端的时钟严格同步;另外,过期日期需要经常检查,一旦过期日期到了,需要在服务器中配置提供一个新的日期。所以,HTTP1.1 引入了 Cache-Control 头来克服 Exipres 头的限制。
Cache-Control 使用 max-age 指令指定组件被缓存多久,它以秒为单位定义了一个更新期限。使用带有 max-age 的 Cache-Control 可以消除 Expires 的限制,但对于不支持 HTTP1.1 的应用(在 2019 的现在很少很少了),仍希望使用 Expires 头。可以同时制定这两个响应头,如果两者同时出现时,HTTP 规范规定 max-age 指令将重写 Expires 头。
当出现了 Expires 头时,直到过期时间为止一直会使用缓存的版本,浏览器不会检查任何更新,直到过了过期时间。为了确保用户能够获取组件的最新版本,需要在所有的 HTML 页面中修改组件的文件名。Yahoo 在此使用了将版本号嵌入在组件的文件名中的方法,简而言之就是通过修改文件名的方式来重置缓存。

使用 Gzip 压缩

压缩组件通过减少 HTTP 请求产生的响应包的大小,从而降低传输时间的方式来提高性能。从 HTTP1.1 开始,Web 客户端可以通过 HTTP 请求中的 Accept-Encoding 头来标识对压缩的支持:

Accept-Encoding: gzip,deflate

如果 Web 服务器看到请求中的这个头,就会使用客户端列出的方法中的一种来压缩响应。Web 服务器通过响应中的 Content-Encoding 头来告知 Web 客户端:

Content-Encoding: gzip

目前许多网站通常会压缩 HTML 文档,脚本和样式表的压缩也是值得的(包括 XML 和 JSON 在内的任何文本响应理论上都值得被压缩)。但是,图片和 PDF 文件不应该被压缩,因为它们本来已经被压缩了。
压缩通常能将响应的数据量减少近 70%,但是压缩通常情况下会带来服务端和客户端的 CPU 开销,要检测受益是否大于开销,需要综合考虑响应大小、带宽和客户端服务端物理距离等因素。通常需要对大于 1KB 或 2KB 的文件进行压缩。
当浏览器通过代理来发送请求时,有可能出现浏览器期望接受的压缩后内容和实际接收到的不一致的情况。解决这一问题的方法是在 Web 服务器的响应中添加 Vary 头。Web 服务器可以告诉代理根据一个或多个请求头来改变缓存的响应。由于压缩的决定是基于 Accept-Encoding 请求头的,因此需要在服务器的 Vary 响应头中包含 Accept-Encoding:

Vary: Accept-Encoding

目前大约 90% 的通过浏览器进行的网络通信都需要使用 gzip,这使得服务端和客户端的对等性变得额外重要。无论是客户端还是服务端发送错误,都会造成页面被破坏。避免错误的一种方式是采用『浏览器白名单』方式,即只为经过证实支持压缩的浏览器提供压缩内容,但是当代理缓存加进来以后,处理边缘情形浏览器将变得更加复杂。另一种方式是使用 Vary: * 或 Cache-Control: private 头来禁用代理缓存。此种方式会为所有浏览器禁用代理缓存,从而增加带宽开销。如何平衡压缩和代理支持需要在加快响应时间、减小带宽开销和边缘情形浏览器缺陷之间进行权衡:

如果网站的用户很少,并且他们处于一个小圈子中,边缘情形浏览器不需要太多关注,可以压缩内容并使用 Vary: Accept-Encoding。
如果更注重带宽开销,可以和前一种情形一样,压缩内容并使用 Vary: Accept-Encoding。
如果网站拥有大量的、多变的用户群,能够应付较高的带宽开销,并且享有高质量的声誉,需要压缩内容并使用 Cache-Control: Private。( Google 和 Yahoo 都使用这种方式)

将 CSS 放在页面顶部

我们研究雅虎网页性能时发现把样式表移到 <head> 里会让页面更快。这是因为把样式表移到 <head> 里允许页面逐步渲染。
关注性能的前端工程师希望页面被逐步渲染,这时因为,我们希望浏览器尽早渲染获取到的任何内容。这对大页面和网速慢的用户很重要。给用户视觉反馈,比如进度条的重要性已经被大量研究和记录。在我们的情况中,HTML 页面就是进度条。当浏览器逐步加载页面头部,导航条,logo 等等,这些都是给等待页面的用户的视觉反馈。这优化了整体用户体验。
把样式表放在文档底部的问题是它阻止了许多浏览器的逐步渲染,包括 IE。这些浏览器阻止渲染来避免在样式更改时需要重绘页面元素。所以用户会卡在白屏。
HTML 规范 清楚表明样式应该在 <head> 里。

将 JavaScript 放在页面底部

脚本会阻塞并行下载,HTTP/1.1 官方文档建议浏览器每个主机名下并行下载的组件数不要超过两个,如果图片来自多个域名,并行下载的数量就可以超过两个。如果脚本正在下载,浏览器就不开始任何其它下载任务,即使是在不同域名下的。
有时候,并不容易把脚本移动到底部。举个例子,如果脚本是用 document.write 插入到页面内容中的,就没办法再往下移了。还可能存在作用域问题,在多数情况下,这些问题都是可以解决的。
一个常见的建议是用推迟(deferred)脚本,有 DEFER 属性的脚本意味着不能含有 document.write,并且提示浏览器告诉他们可以继续渲染。不幸的是,Firefox 不支持 DEFER 属性。在 IE 中,脚本可能被推迟,但不尽如人意。如果脚本可以推迟,我们就可以把它放到页面底部,页面就可以更快地载入。

避免使用 CSS 表达式

CSS 表达式是强大的(可能也是危险的)设置动态 CSS 属性的方法。IE5 开始支持,IE8 开始不赞成使用。例如,背景颜色可以设置成每小时轮换:

background-color: expression( (new Date()).getHours()%2 ? "#B8D4FF" : "#F08A00" );

表达式的问题在于它们的评估频率高于大多数人的预期。它们不仅在页面呈现和调整大小时进行重新计算,而且在页面滚动时甚至在用户将鼠标移动到页面上时进行计算。在 CSS 表达式中添加计数器可以让我们跟踪 CSS 表达式的计算时间和频率。在页面上移动鼠标可以轻松计算超过 10,000 次。

使用外部 JavaScript 和 CSS

在现实环境中使用外部文件通常会产生较快的页面,因为 JavaScript 和 CSS 有机会被浏览器缓存起来。对于内联的情况,由于 HTML 文档通常不会被配置为可以进行缓存的,所以每次请求 HTML 文档都要下载 JavaScript 和 CSS。所以,如果 JavaScript 和 CSS 在外部文件中,浏览器可以缓存它们,HTML 文档的大小会被减少而不必增加 HTTP 请求数量。
决定是否使用外部文件的关键在于被缓存的外部文件占请求的 HTML 文档数的比重。如果网站用户在每次会话中进行多次页面访问,同时页面重用了多个脚本和样式表,使用外部文件时很好的选择。
对于大多数网站而言,难以精确度量以判断是否使用内联或外部文件,此时建议是使用外部文件的方式。对于这个问题的一个例外是网站主页,由于主页对于响应时间要求更高,因此更加倾向于内联而不是外部文件。
对于内联文件而言,由于无法利用浏览器缓存,因此给人感觉依然比较低效。我们可以通过加载后下载和动态内联的方式来使得网站主页既可以获得内联的优势,同时也能缓存外部文件。

减少 DNS 查询

用户输入 URL 以后,浏览器首先要查询域名(example.com)对应服务器的 IP 地址,这个操作一般需要耗费 20-120 毫秒时间。DNS 查询完成之前,浏览器无法从服务器下载任何数据。
基于性能考虑,ISP、局域网、操作系统、浏览器都会有相应的 DNS 缓存机制。

  • IE 缓存 30 分钟,可以通过注册表中 DnsCacheTimeout 项设置;
  • Firefox 缓存 1 分钟,通过 network.dnsCacheExpiration 配置;
  • Chrome 缓存 1 分钟,通过 chrome://net-internals/#dns 配置。

另外需要注意的是这里有一个矛盾,减少不同的域名可减少 DNS 查找,同时也减少了页面下载资源文件的并发量。也就是说,虽然避免 DNS 查找削减了响应时间,但是减少并行下载数量却增加了响应时间。原则是把组件分散在 2~4 个域名下,控制好数量,这是同时减少 DNS 查找和允许并发下载的折中方案。

压缩 JavaScript 和 CSS

压缩具体来说就是从代码中去除不必要的字符以减少大小,从而提升加载速度。代码最小化就是去掉所有注释和不必要的空白字符(空格,换行和 tab)。
在 JavaScript 中这样做能够提高响应性能,因为要下载的文件变小了。两个最常用的 JavaScript 代码压缩工具是 JSMin 和 YUI Compressor,YUI compressor 还可以压缩 CSS。
混淆 是一种可选的源码优化措施,要比压缩更复杂,所以混淆过程也更容易产生 Bug。在对美国前十的网站调查中,压缩可以缩小 21%,而混淆能缩小 25%。虽然混淆的缩小程度更高,但比压缩风险更大。
除了压缩外部脚本和样式,行内的和块也可以压缩。即使启用了 gzip 模块,先进行压缩也能够缩小 5% 或者更多的大小。JavaScript 和 CSS 的用处越来越多,所以压缩代码会有不错的效果。
得益于 Node.js 的流行,开源社区涌现出许多高效、易用的前端优化工具,JavaScript 和 CSS 压缩类如 UglifyJS 2、csso、cssnano 等都是很成熟的工具。
对于内嵌的 CSS 和 JavaScript,也可以通过 htmlmin 等工具压缩。
这些项目都有 Gulp、Webpack 等流行构建工具的配套版本。

避免 301/302 重定向

说明
HTTP 重定向通过 301/302 状态码实现。下面是一个 301 状态码的 HTTP 头:

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

浏览器会自动跳转到 Location 域指明的 URL。重定向需要的所有信息都在 HTTP 头部,而响应体一般是空的。其实额外的 HTTP 头,比如 Expires 和 Cache-Control 也表示重定向。除此之外还有别的跳转方式:refresh 元标签和 JavaScript,但如果你必须得做重定向,最好用标准的 3xx HTTP 状态码,主要是为了让返回按钮能正常使用。
影响
客户端收到服务器的重定向响应后,会根据响应头中 Location 的地址再次发送请求。重定向会影响用户体验,尤其是多次重定向时,用户在一段时间内看不到任何内容,只看到浏览器进度条一直在刷新。
常见错误
有一种常见的极其浪费资源的重定向,而且 Web 开发人员一般都意识不到这一点:URL 末尾应该添加 / 但未添加。比如,访问 http://astrology.yahoo.com/astrology 将被 301 重定向到 http://astrology.yahoo.com/astrology/(注意末尾的 /)。如果使用 Apache,可以通过 Alias 或 mod_rewrite 或 DirectorySlash 解决这个问题。
另一种常见用途是从旧站点跳转到新站点,有时是跳转到同一站点的不同部分,或者针对用户的不同情况(浏览器类型,用户帐号类型等等)做一些处理。用重定向来连接两个网站是最简单的,只需要少量的额外代码。虽然在这些时候使用重定向减少了开发人员的开发复杂度,但降低了用户体验。一种替代方案是用 Alias 和 mod_rewrite,前提是两个代码都在相同的服务器上。如果是因为域名变化而使用了重定向,就可以创建一条 CNAME(创建一个指向另一个域名的 DNS 记录作为别名)结合 Alias 或者 mod_rewrite 指令。

移除重复的 JavaScript 脚本

页面含有重复的脚本文件会影响性能,这可能和你想象的不一样。在对美国前 10 大 Web 站点的评审中,发现只有 2 个站点含有重复脚本。两个主要原因增加了在单一页面中出现重复脚本的几率:团队大小和脚本数量。在这种情况下,重复脚本会创建不必要的 HTTP 请求,执行无用的 JavaScript 代码,而影响页面性能。
IE 会产生不必要的 HTTP 请求,而 Firefox 不会。在 IE 中,如果一个不可缓存的外部脚本被页面引入了两次,它会在页面加载时产生两个 HTTP 请求。即使脚本是可缓存的,在用户重新加载页面时也会产生额外的 HTTP 请求。
除了产生没有意义的 HTTP 请求之外,多次执行脚本也会浪费时间。因为无论脚本是否可缓存,在主流浏览器中都会执行冗余的 JavaScript 代码。
避免不小心把相同脚本引入两次的一种方法就是在模板系统中实现脚本管理模块。典型的脚本引入方法就是在 HTML 页面中用 SCRIPT 标签:

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

升级为以下这种 PHP 动态函数 insertScript() 进行加载:

<?php insertScript("menu.js") ?>

这样在 insertScript() 里执行确保加载的唯一性,或者设置好文件版本等操作。

设置 ETags 来控制缓存

实体标记(Entity tags,ETag)是服务器和浏览器之间判断浏览器缓存中某个文件是否匹配服务器端原文件的一种机制。实体就是资源文件,如图片,脚本,样式等等。ETag 是比验证 last-modified 日期更高效的机制。
服务器这样设置组件的 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(文件未变化),就不下载内容。这样就减少了 12195 个字节的响应体。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 中嵌入的数据都会大大降低有效性验证的成功率。
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 不匹配,用户就不会按照 ETag 的设计那样接收到更小更快的 304 响应;相反,它们会收到普通的 200 响应以及组件的所有数据。
如果只在一台服务器上部署网站,这通常不会产生问题;但如果使用了服务器集群,同时使用 Apache 或者 IIS 默认的 ETag 配置,用户响应将变慢,服务器负载将变高,将消耗更多的带宽,同时代理缓存的效率也会下降。即使组件具有长久的 Expires 头,一旦用户单击了 Reload 或 Refresh 按钮,依然会产生条件 GET 请求。
如果组件必须通过最新修改日期之外的一些东西来进行验证,则 ETag 是一种强大的方法;如果无须自定义 ETag,则最好将其移除。 last-modified 头基于文件的时间戳进行验证,可以提供完全等价的信息,而且移除 ETag 可以减少响应和后续请求的 HTTP 头的大小。在 Apache 中,只要向 Apache 配置文件中简单地添加下面一行配置就能移除 ETag:

FileETag none

缓存 Ajax 请求

Ajax 的一个好处是可以给用户提供即时反馈,因为它能够从后台服务器异步请求数据。然而,用了 Ajax 就无法保证用户在等待异步 JavaScript 和 XML 响应返回期间不会非常无聊。在很多应用程序中,用户能够一直等待取决于如何使用 Ajax。例如,在基于 Web 的电子邮件客户端中,用户为了寻找符合他们搜索标准的邮件消息,将会保持对 Ajax 请求返回结果的关注。重要的是,要记得『异步』并不意味着『即时』。
要提高性能,优化这些 Ajax 响应至关重要。最重要的提高 Ajax 性能的方法就是让响应变得可缓存,就像在 添上 Expires 或者 Cache-Control HTTP 头 中讨论的一样。下面适用于 Ajax 的其它规则:

  • Gzip 压缩
  • 减少 DNS 查找
  • 压缩 JavaScript
  • 避免重定向
  • 配置 ETags

我们一起看看例子,一个 Web 2.0 的电子邮件客户端用了 Ajax 来下载用户的通讯录,以便实现自动完成功能。如果用户从上一次使用之后再没有修改过她的通讯录,而且 Ajax 响应是可缓存的,有尚未过期的 Expires 或者 Cache-Control HTTP 头,那么之前的通讯录就可以从缓存中读出。必须通知浏览器,应该继续使用之前缓存的通讯录响应,还是去请求一个新的。可以通过给通讯录的 Ajax URL 里添加一个表明用户通讯录最后修改时间的时间戳来实现,例如 &t=1190241612。如果通讯录从上一次下载之后再没有被修改过,时间戳不变,通讯录就将从浏览器缓存中直接读出,从而避免一次额外的 HTTP 往返消耗。如果用户已经修改了通讯录,时间戳也可以确保新的 URL 不会匹配缓存的响应,浏览器将请求新的通讯录条目。
即使 Ajax 响应是动态创建的,而且可能只适用于单用户,它们也可以被缓存的,而这样会让你的 Web 2.0 应用更快。

尽早输出(flush)缓冲

当用户请求一个页面时,服务器需要用大约 200 到 500 毫秒来渲染 HTML 页面,在这期间,浏览器闲等着数据到达。PHP 中有一个 flush() 函数,可以发送部分已经准备好的 HTML 到浏览器,以便服务器还在忙于处理剩余页面时,浏览器可以提前开始获取资源。
可以考虑在之后输出一次缓冲,HTML head 一般比较容易生成,先发送以便浏览器开始获取引用的 CSS 等资源。
例如:

<!-- css, js -->
</head>
<?php flush(); ?>
<body>
<!-- content -->

Ajax 时尽量使用 GET 方法

使用 XMLHttpRequest 时,浏览器的 POST 请求是通过一个两步的过程来实现的:先发送 HTTP 头再发送数据。所以最好用 GET 请求,它只需要发送一个 TCP 报文(除非 Cookie 特别多)。
IE 的 URL 长度最大值是 2K,所以如果要发送的数据超过 2K 就无法使用 GET 了。
POST 在不提交任何数据的情况下跟 GET 行为很类似,但从语义上讲,获取数据应该用 GET,提交数据到服务器用 POST。

延迟加载

问自己一个问题:页面初始加载时哪些内容是绝对必需的?
不在答案之列的资源都可以延迟加载。比如:

  • 非首屏使用的数据、样式、脚本、图片等;
  • 用户交互时才会显示的内容。

遵循「渐进增强」理念开发的网站:JavaScript 用于增强用用户体验,但没有(不支持) JavaScript 也能正常工作,完全可以延迟加载 JavaScript。
将首屏以外的 HTML 放在不渲染的元素中,如隐藏的 <textarea>,或者 type 属性为非执行脚本的 <script> 标签中,减少初始渲染的 DOM 元素数量,提高速度。等首屏加载完成或者用户操作时,再去渲染剩余的页面内容。
另一个常见的做法是图片的延迟加载(懒加载),具体逻辑是当用户滚动页面看到内容时,再加载所需图片。这种操作在较多图片的网页中,可以节省大量带宽,页面渲染速度也会变快。

预加载

预加载可能看起来和延迟加载是相反的,但它其实有不同的目标。通过预加载组件可以充分利用浏览器空闲的时间来请求将来会用到的组件(图片,样式和脚本)。用户访问下一页的时候,大部分组件都已经在缓存里了,所以在用户看来页面会加载得更快。
实际应用中有以下几种预加载的类型:

  • 无条件预加载 —— 尽快开始加载,获取一些额外的组件。google.com 就是一个 sprite 图片预加载的好例子,这个 sprite 图片并不是 google.com 主页需要的,而是搜索结果页面上的内容。
  • 条件性预加载 —— 根据用户操作猜测用户将要跳转到哪里并据此预加载。在 search.yahoo.com 的输入框里键入内容后,可以看到那些额外组件是怎样请求加载的。
  • 提前预加载 —— 在推出新设计之前预加载。经常在重新设计之后会听到:“这个新网站不错,但比以前更慢了”,一部分原因是用户访问先前的页面都是有旧缓存的,但新的却是一种空缓存状态下的体验。可以通过在将要推出新设计之前预加载一些组件来减轻这种负面影响,老站可以利用浏览器空闲的时间来请求那些新站需要的图片和脚本。

减少 DOM 元素数量

复杂的页面不仅下载的字节更多,JavaScript DOM 操作也更慢。例如,同是添加一个事件处理器,500 个元素和 5000 个元素的页面速度上会有很大区别。
从以下几个角度考虑移除不必要的标记:

  • 是否还在使用表格布局?
  • 塞进去更多的 <div> 仅为了处理布局问题?也许有更好、更语义化的标记。
  • 能通过伪元素实现的功能,就没必要添加额外元素,如清除浮动。

浏览器控制台中输入以下代码可以计算出页面中有多少 DOM 元素:

document.getElementsByTagName('*').length;

为什么不使用表格布局?

  • 更多的标签,增加文件大小;
  • 不易维护,无法适应响应式设计;
  • 性能考量,默认的表格布局算法会产生大量重绘

使用多个域名

Chrome 等现代化浏览器,都会有同域名限制并发下载数的情况,不同的浏览器及版本都不一样,简单的情况如下:

浏览器版本 每个域名并发连接数
Chrome34/32 6
IE10 8
IE11 13
Firefox27/26 6
Safari7.0.1 6

浏览器更新日新月异,以上数据并不一定完全正确,但是作为参考足够了。
使用不同的域名可以最大化下载线程,但注意保持在 2~4 个域名内,以避免 DNS 查询损耗。
例如,动态内容放在 csspod.com 上,静态资源放在 static.csspod.com 上。这样还可以禁用静态资源域下的 Cookie,减少数据传输,详见 Cookie 优化。

避免使用 iframe

用 iframe 可以把一个 HTML 文档插入到父文档里,重要的是明白 iframe 是如何工作的并高效地使用它。
<iframe> 的优点:
可以用来加载速度较慢的第三方资源,如广告、徽章;
可用作安全沙箱;
可以并行下载脚本。

<iframe> 的缺点:
加载代价昂贵,即使是空的页面;
阻塞页面 load 事件触发;
Iframe 完全加载以后,父页面才会触发 load 事件。 Safari、Chrome 中通过 JavaScript 动态设置 iframe src 可以避免这个问题。
缺乏语义。

杜绝 404

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

Cookie 被用于身份认证、个性化设置等诸多用途。Cookie 通过 HTTP 头在服务器和浏览器间来回传送,减少 Cookie 大小可以降低其对响应速度的影响。

  • 去除不必要的 Cookie;
  • 尽量压缩 Cookie 大小;
  • 注意设置 Cookie 的 domain 级别,如无必要,不要影响到 sub-domain;
  • 设置合适的过期时间。

当浏览器请求静态图片并把 Cookie 一起发送到服务器时,Cookie 此时对服务器没什么用处。这些 Cookie 只是增加了网络流量。所以你应该保证静态文件的请求是没有 Cookie 的。可以创建一个子域名来托管所有静态组件。
比如,你域名是 www.example.org,可以把静态组件托管在 static.example.org。不过,你如果把 Cookie 设置在顶级域名 example.org 下,这些 Cookie 仍然会被传给 static.example.org。这种情况下,可以考虑启用一个全新的域名来处理静态文件请求。
另外一个使用不带 Cookie 的域名的好处是,某些代理可能会阻止缓存待 Cookie 的静态文件请求。

减少 DOM 操作

JavaScript 操作 DOM 很慢,尤其是当 DOM 节点很多时。
使用时应该注意:

  • 缓存已经访问过的元素;
  • 使用 DocumentFragment 暂存 DOM,整理好以后再插入 DOM 树;
  • 使用 className 来操纵元素的样式;
  • 避免使用 JavaScript 修复布局。

使用高效的事件处理

有时候感觉页面反映不够灵敏,是因为有太多频繁执行的事件处理器被添加到了 DOM 树的不同元素上,这就是推荐使用事件委托的原因。
如果一个 div 里面有 10 个按钮,应该只给 div 容器添加一个事件处理器,而不是给每个按钮都添加一个。事件能够冒泡,所以可以捕获事件并得知哪个按钮是事件源。
另外,你不必等到 onload 事件来开始处理 DOM 树,DOMContentLoaded 更快。大多时候你需要的只是想访问的元素已在 DOM 树中,所以你不必等到所有图片被下载。

使用替代@import

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

不要使用 filter

AlphaImageLoader 为 IE5.5-IE8 专有的技术,和 CSS 表达式一样,放进博物馆吧。IE 专有的 AlphaImageLoader 滤镜可以用来修复 IE7 之前的版本中半透明 PNG 图片的问题。在图片加载过程中,这个滤镜会阻塞渲染,卡住浏览器,还会增加内存消耗而且是被应用到每个元素的,而不是每个图片,所以会存在一大堆问题。
注意!!!这里所说的不是 CSS3 Filter

优化图片

在设计师建好图片后,在上传图片到服务器前你仍可以做些事:

  • 检查 gif 图片的调色板大小是否匹配图片颜色数。
  • 可以把 gif 转成 png 看看有没有变小。除了动画,gif 一般可以转成 png8。
  • 运行 pngcrush 或其它工具压缩 png。
  • 运行 jpegtran 或其它工具压缩 jpeg。

优化 CSS Sprite

  1. 在 Sprite 图片中横向排列一般都比纵向排列的最终文件小;
  2. 组合 Sprite 图片中的相似颜色可以保持低色数,最理想的是 256 色以下 PNG8 格式
  3. “对移动端友好”,不要在 Sprite 图片中留下太大的空隙。虽然不会在很大程度上影响图片文件的大小,但这样做可以节省用户代理把图片解压成像素映射时消耗的内存。100×100 的图片是 1 万个像素,而 1000×1000 的图片就是 100 万个像素了。

不要在 HTML 中缩放图片

不要使用 <img> 的 width、height 缩放图片,如果用到小图片,就使用相应大小的图片。如果需要

<img width="100" height="100" src="mycat.jpg" alt="My Cat" />

那么图片本身(mycat.jpg)应该是 100x100px 的,而不是去缩小 500x500px 的图片。
很多 CMS 和 CDN 都提供图片裁切功能。
补充:设置图片的宽和高,以免浏览器按照「猜」的宽高给图片保留的区域和实际宽高差异,产生重绘。

Favicon.ico

一般存放在网站根目录下,无论是否在页面中设置,浏览器都会尝试请求这个文件。
所以确保这个图标:

  • 存在(避免 404);
  • 尽量小,最好小于 1K;
  • 设置较长的过期时间。
  • 对于较新的浏览器,可以使用 PNG 格式的 favicon。

文件不要大于 25K

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

分段(multipart)文档

把各个组件打包成一个像有附件的电子邮件一样的复合文档里,可以用一个 HTTP 请求获取多个组件(记住一点:HTTP 请求是代价高昂的)。用这种方式的时候,要先检查用户代理是否支持(iPhone 就不支持)。

避免图片 src 为空

图片标签的 src 属性值为空字符串可能以下面两种形式出现:
HTML:

<img src="" />

JavaScript:

var img = new Image(); 
img.src = "";

虽然 src 属性为空字符串,但浏览器仍然会向服务器发起一个 HTTP 请求:

  • IE 向页面所在的目录发送请求;
  • Safari、Chrome、Firefox 向页面本身发送请求;
  • Opera 不执行任何操作。

空 src 产生请求的后果不容小觑:

  • 给服务器造成意外的流量负担,尤其时日 PV 较大时;
  • 浪费服务器计算资源;
  • 可能产生报错。
    空的 href 属性也存在类似问题。用户点击空链接时,浏览器也会向服务器发送 HTTP 请求,可以通过 JavaScript 阻止空链接的默认的行为。

参考链接

35 条前端性能优化军规