Yahoo对Web前端优化的14条经验拔萃

(编辑:jimmy 日期: 2024/12/27 浏览:2)

规则1——减少HTTP请求(Minimize HTTP Requests)
    只有10%~20%的最终用户响应时间花在接收请求的HTML文档上,剩下的80%~90%时间都花在HTML文档所引用的所有组件(图片、脚本、样式表、Flash等)进行的HTTP请求上。因此,改善响应时间最简单的办法就是减少组件数量并由此减少HTTP请求数。减少组件数量通常会和产品设计的初衷相矛盾,因此,此处给出了一些技术:

   图片地图(Image Maps)联合多个图片到一个单独的图片中。下载图片大小的总和保持不变,但是,通过减少HTTP请求数的方式加速了页面。图片地图适用于导航栏或其他超链接中使用多个图片的情形。但是,在定义图片地图上的区域坐标时,如果采用手工方式很难完成且容易出错,而且除了矩形外几乎无法定义其他形状。

   CSS Sprites使用CSS background-image和background-position属性将多个图片联合成一个独立的图片来显示。它通过合并图片减少了HTTP请求,并且比图片地图更加灵活,同时也降低了图片的下载量。如果在页面中需要为背景、按钮、导航栏、链接等提供大量图片,CSS Sprites是一种优秀的解决方案。

   内联图片(Inline images)使用data: URL scheme模式将图片嵌入到HTML文档中。通过此模式嵌入图片,不需要任何额外的HTTP请求开销。但是,目前的主流浏览器(主要是IE)不支持此种方式。

   合并文件(Combined files)通过将所有JavaScript脚本合并到一个文件,所有CSS样式表合并到另一个文件的方式来减少HTTP请求的数量。但是简单的合并通常会遇到模块化、页面变化等问题,需要根据页面引用脚本和样式表来具体分析以确定具体的组合方式。

规则2——使用内容发布网络(Use a Content Delivery Network)
    用户同Web服务器的距离会对页面响应时间产生影响。网站最初通常将其所有服务器放在同一个地方,当用户群增加时,公司必须面对服务器放置地点不再合适的事实。因此,有必要在多个地理位置不同的服务器上部署内容。

   作为实现地理位置分离的第一步,不应当首先尝试使用分布式架构重新设计Web应用程序。这样的应用程序决定了重新设计将带来如同步会话状态、在服务器放置地点间复制数据库事务等复杂问题。重新设计会推迟甚至根本无法实现缩短用户和网站内容距离的愿望。

   如果应用程序Web服务器里用户更近,则一个HTTP请求的响应时间将被缩短;如果组件Web服务器离用户更近,则多个HTTP请求的响应时间将缩短。因此,与其重新开始设计应用程序,以便将应用程序Web服务器分散开,不如首先将组件Web服务器分散开。这不仅能达到响应时间大幅减少的目的,还很容易实现。

   内容发布网络(CDN)是一组分布在多个不同地理位置的Web服务器,用于更加有效的向用户发布内容。向特定用户发布内容的服务器基于对网络可用度的测量,例如,CDN可能选择网络阶跃数最小的服务器,或者具有最短响应时间的服务器。

   除了缩短响应时间外,CDN还可以带来其他优势,包括备份、扩展存储能力和进行缓存;同时,CDN还有助于缓和Web流量峰值的压力,如在获取天气或股市新闻、浏览体育或娱乐事件时。依赖CDN的一个缺点是网站的响应时间会受到其他网站——甚至可能是竞争对手流量的影响;另一个缺点是无法直接控制组件服务器所带来的问题。

  CDN用于发布静态内容(如图片、脚本、样式表、Flash)。提供动态HTML页面会引入特殊的存储要求——数据库连接、状态管理、验证、硬件和OS优化等,这些复杂性超过了CDN的范围。另一方面,静态文件更容易存储并具有较少的依赖。

规则3——添加Expires头(Add an Expires or a Cache-Control Header)
   Web页面包含大量组件,并且数量在不断增长。页面的初访者会进行很多的HTTP请求,但通过一个长久的Expires头,可以使这些组件被缓存下来,可以在后续的页面浏览中避免不必要的HTTP请求。长久的Expires头最长用于图片,但应该将其用于所有组件上,包括脚本、样式表和Flash。

Web服务器使用Expires头告诉Web客户端它可以使用一个组件的当前副本,知道指定时间为止。HTTP规范中简要的称该头为“在这一日期/时间之后,响应将被认为是无效的”。例如:

复制代码代码如下:
Expires: Thu, 15 Apr 2010 20:00:00 GMT

告诉浏览器该响应的有效性持续到2010年4月15日。

因为Expires头使用一个特定的时间,它要求服务端和客户端的时钟严格同步;另外,过期日期需要经常检查,一旦过期日期到了,需要在服务器中配置提供一个新的日期。所以,HTTP1.1引入了Cache-Control头来克服Exipres头的限制。Cache-Control使用max-age指令指定组件被缓存多久,它以秒为单位定义了一个更新窗。使用带有max-age的Cache-Control可以消除Expires的限制,但对于不支持HTTP1.1的应用,仍希望使用Expires头。可以同时制定这两个响应头,如果两者同时出现时,HTTP规范规定max-age指令将重写Expires头。

当出现了Expires头时,直到过期时间为止一直会使用缓存的版本,浏览器不会检查任何更新,直到过了过期时间。为了确保用户能够获取组件的最新版本,需要在所有的HTML页面中修改组件的文件名。Yahoo在此使用了将版本号嵌入在组件的文件名中的方法。

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

如果网站的用户很少,并且他们处于一个小圈子中,边缘情形浏览器不需要太多关注,可以压缩内容并使用Vary: Accept-Encoding。

如果更注重带宽开销,可以和前一种情形一样,压缩内容并使用Vary: Accept-Encoding。

如果网站拥有大量的、多变的用户群,能够应付较高的带宽开销,并且享有高质量的名声,需要压缩内容并使用Cache-Control: Private。(Google和Yahoo都使用这种方式)

规则5——将样式表放在顶部(Put Stylesheets at the Top)
我们都希望页面能够逐步加载,也就是说,我们希望浏览器能够尽快显示内容。当浏览器逐步加载页面时,页头、导航栏、顶端logo等所有这些都会等待页面的用户提供视觉反馈,这改善了用户体验。将样式表放在底部,为避免当样式变化时重绘页面中的元素,浏览器会阻塞内容逐步呈现。

样式表在页面中的位置并不影响下载时间,但是会影响页面的呈现。根据HTML规范“和A不一样,[LINK]只能出现在文档的HEAD节中,但其出现次数是任意的”。因此,问题的解决方式应该是遵循HTML规范,使用LINK标签将样式表放在文档的HEAD中。

规则6——将脚本放在底部(Put Scripts at the Bottom)
对响应时间影响最大的是页面中的组件数量,而脚本会阻塞组件的并行下载,带来性能上的问题。HTTP1.1规范建议浏览器从每个主机名并行下载两个组件。如果一个Web页面平均将其组件分别放在两个主机名下,整体响应时间可以减少大约一半。我们可以通过对浏览器默认设置的修改来增加每个主机名并行下载组件的数量,也可以使用CNAME(DNS别名)将组件分别放到多个主机名下。但是,增加并行下载数量通常会带来性能上的开销,过多的并行下载有时反而会降低性能。Yahoo!研究表明,使用两个主机名比使用1、4或10个主机名能带来更好的性能。

需要我们注意的是,下载脚本时并行下载实际上是被禁用的,即使此时使用了不同的主机名,浏览器也不会启动其他下载。因此,如果将脚本放在顶部,脚本会阻塞后面内容的呈现,也会阻塞后面组件的下载。因此,放置脚本最好的地方是页面底部,这不会阻止页面内容呈现,而且页面中的可视组件可以尽早下载。

规则7——避免CSS表达式(Avoid CSS Expressions)
CSS表达式是动态设置CSS属性的一种强大(并且危险)的方式。它从IE5以后的版本被支持,在IE8中已经被废弃。
表达式的问题在于对其进行的求值频率比人们期望的要高。它们不只在呈现页面和大小改变时求值,当页面滚动,甚至用户鼠标在页面上移过时都要进行求值。

减少CSS表达式求值次数的一种方式是使用一次性表达式,如果CSS表达式必须被求值一次,可以在这一次中执行重写它本身。除此之外,还可以使用事件处理器来为特定的事件提供所期望的动态行为。

规则8——使用外部JavaScript和CSS(Make JavaScript and CSS External)
在现实环境中使用外部文件通常会产生较快的页面,因为JavaScript和CSS有机会被浏览器缓存起来。对于内联的情况,由于HTML文档通常不会被配置为可以进行缓存的,所以每次请求HTML文档都要下载JavaScript和CSS。所以,如果JavaScript和CSS在外部文件中,浏览器可以缓存它们,HTML文档的大小会被减少而不必增加HTTP请求数量。

决定是否使用外部文件的关键在于被缓存的外部文件占请求的HTML文档数的比重。如果网站用户在每次会话中进行多次页面访问,同时页面重用了多个脚本和样式表,使用外部文件时很好的选择。

对于大多数网站而言,难以精确度量以判断是否使用内联或外部文件,此时建议是使用外部文件的方式。对于这个问题的一个例外是网站主页,由于主页对于响应时间要求更高,因此更加倾向于内联而不是外部文件。

对于内联文件而言,由于无法利用浏览器缓存,因此给人感觉依然比较低效。我们可以通过加载后下载和动态内联的方式来使得网站主页既可以获得内联的优势,同时也能缓存外部文件。


规则9——减少DNS查找(Reduce DNS Lookups)
DNS对于网站来说会带来开销。通常浏览器查找一个给定主机名的IP要花费20~120毫秒的时间。在DNS查找完成之前,浏览器不能从此主机下载任何东西。

DNS查找可以被缓存起来以提高性能,这种缓存可以发生在ISP或局域网中的一台特殊的缓存服务器上,同时,缓存也会发生在独立的用户机器上。在用户请求一个主机名后,DNS信息会留在操作系统的DNS缓存中,大多数浏览器也拥有自己的缓存,和操作系统缓存相分离。只要浏览器在其缓存中保留了DNS记录,就不会通过操作系统来请求这个记录。

当客户端浏览器和操作系统中DNS缓存同时为空时,DNS查询的数量等于页面中唯一主机名的数量,这些主机名包括了页面的URL、图片、脚本、样式表、Flash等。所以,减少唯一主机名数量,可以减少DNS查询数。

减少唯一主机名数量会潜在的减少页面中并行下载的数量。避免DNS查找降低了响应时间,但减少并行下载可能会增加响应时间。对于这种情形,建议将这些组件放在至少2个,但不要超过4个主机名下。

规则10——精简JavaScript和CSS(Minify JavaScript and CSS)
精简是从代码中移除不必要的字符以减小其大小,进而改善加载时间。在代码被精简后,所有注释以及不必要的空白字符(空格、换行和制表符)都将被移除。对于JavaScript而言,因为需要下载的文件大小减小了,可以改善响应时间。

混淆是可以应用在源代码上的另一种优化方式。相比较于精简,混淆更加复杂,因此更容易产生bug。混淆可以更大程度上压缩源代码,但是也存在着一定的风险。

除了外部JavaScript外,内联在<script>和<style>块中的源代码也需要被精简。即使使用了gzip来压缩JavaScript和CSS,使用精简能够将代码大小再减少5%或者更多。

规则11——避免重定向(Avoid Redirects)
重定向用于将用户从一个URL路由到另一个URL。重定向有很多种,其中301和302是最常用的两种。下面是一个301响应头的示例:

复制代码代码如下:
HTTP/1.1 301 Moved Permanently
Location: http://example.com/newuri
Content-Type: text/html

浏览器会自动将用户带到Location字段给出的URL。重定向所必需的所有信息都包含在这个头中,响应体通常是空的。不管叫什么名字,301或者302响应在实际中都不会被缓存,除非有附加的头(如Expires或Cache-Control等)要求它这么做。meta refresh标签和JavaScript也可以用于重定向,但是最好的技术是使用标准的3xx状态码,以保证后退按钮能够正常工作。

需要我们记住的是重定向会使页面变慢。在用户和HTML文档间插入一个重定向后,在此HTML文档到达之前页面上不会描绘任何东西,任何组件也不会被下载。

有一种重定向最为浪费,发生的也很频繁,但是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。

规则12——移除重复脚本(Remove Duplicate Scripts)
在一个页面中两次保护同一个JavaScript文件会损伤性能。导致一个脚本重复的因素主要有两个——团队大小和脚本数量。

当重复脚本的现象发生时,将产生不必要的HTTP请求和浪费执行JavaScript的时间。不必要的HTTP请求会发生在IE中,而不会发生在Firefox中。在IE中,如果脚本被包含两次且没有被缓存,浏览器会在页面加载期间产生两个HTTP请求;即使脚本可以缓存,当用户重新加载页面时也会产生额外的HTTP请求。对JavaScript进行的多余的执行从而浪费时间的现象在IE和Firefox中都存在,与脚本是否被缓存无关。

避免意外包含同一脚本两次的一种方法是在你的模块系统中实现一个脚本管理模块。包含脚本的典型方式是在HTML页面中使用SCRIPT标签:

JavaScript Code复制内容到剪贴板
  1. <script type=”text/javascript” src=”menu_1.0.17.js”></script>  

另一种选择是在PHP中创建一个函数:

PHP Code复制内容到剪贴板
  1. <?php insertScript(“menu.js”) ?>  

为了防止统一个脚本被重复添加多次,insertScript函数需要添加处理脚本的依赖性和版本的功能。

规则13——配置Etag(Configure ETags)
实体标签(Entity Tag,ETag)是Web服务器和浏览器用于确认缓存组件的有效性的一种机制。ETag在HTTP1.1中引入,用于检测浏览器缓存中的组件与原始服务器上的组件是否匹配。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状态码,在此例中使响应减少12195字节。

复制代码代码如下:
GET /i/yahoo.gif HTTP/1.1
Host: us.yimg.com
f-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

规则14——使Ajax可缓存(Make Ajax Cacheable)
Ajax的一个最重要的优点就是向用户提供即时反馈,因为它异步的从后台Web服务器请求信息。但是,使用Ajax并不保证用户不会等到异步的JavaScript和XML返回响应。在很多应用程序中,用户是否需要等待取决于如何使用Ajax。用户是否需要等待的关键因素在于Ajax请求是主动的还是被动的。主动请求是基于用户的当前操作而发起的,被动请求则是为了将来使用而预先发起的。我们需要注意的是,“异步”并没有暗示“实时”。

为了提升性能,最重要的是优化Ajax响应。而改善这些主动Ajax请求的最重要的方式就是使响应可缓存。如同在“添加Expires头”中讨论的,一些其他规则也适用于Ajax,包括:压缩组件、减少DNS查找、精简JavaScript、避免重定向、配置Etag。

PS:具体的关于优化的知识点或问题

1. 为什么网页设置缓存后仍然有请求(304响应)?

浏览器刷新是conditional request,所以如果通过刷新来看缓存是否有效肯定是304。可以试试输入网址按回车或者回退键来看效果。另外由于HTML文档很少设置完全缓存(一般要和服务器验证),可以看静态组件的缓存效果(200 ok (from cache))。

2. expirationTime = responseTime + freshnessLifetime - currentAge

freshnessLifetime具体怎么算可以参考https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching_FAQ。

3. Flash of unstyled content(无样式内容闪烁)

Flash of unstyled content(FOUC)就是在加载外部样式表之前,浏览器按默认样式显示了内容,这是因为浏览器在所有资源都下载好前就开始渲染页面了。一旦外部样式被加载,浏览器就会修正样式,但这种修正可能是可见的,也就是FOUC。

怎么避免?在<head>中通过<link>引入样式,避免使用@import。