高性能网站 首屏渲染速度

l1、减少HTTP请求,点击一个图片会跳转相应的页面  这时会有5个http请求

             可以使用图片地图使用一张图片在响应跳转位置进行映射跳转到响应的位置,这时减少了4个http请求

      优点:减少HTTP请求,图片中的子元素图片需要和设计图一致,不可随意排列

      缺点:在定义图片地图上的区域坐标时,容易出错,并且响应式兼容也是个问题

      1.2、CSS Sprites    (CSS 精灵)

               和图片地图一样,CSS Sprites也可以合并图的需要和设计图上一致,而CSS Sprites则没有这个限制,只需要改变背景                   图片的偏移量等属性即可

       优点:减少HTTP请求,相对于图片地图更加灵活,降低下载量(很多人会认为合并之后的图片会比分离的图片的总和要                              大,因为合并的图片中包含有许多空白区域,实际上,合并后的图片会比分离的图片的总和要小,这是因为它降低                            了图片自身的开销,如颜色表,格式信息等等)

       缺点:响应问题不好解决,容易出现图片位置偏差的情况

       1.3、内联图片

                通过data:URL模式可以在Web页面中包含图片但无需任何额外的HTTP请求。尽管IE8以下不支持,IE8以上支持但是                    大小有限制,但它能给其他浏览器带来的节省使得它值得关注

                data:URL模式在1995年被首次提议规范对它的描述为:“允许将小块数据内联为‘立即数’”。数据就在其URL自身之中,                  其格式如下 

                data:[<mediatype][;base64],<data>

        目前通常是使用base64图片以下是它的优缺点

        优点:减少HTTP请求

        缺点:根据base64的编码原理,大小比原文件大3/1,如果将图片放到html中里面会减慢首屏加载,图片不会被缓存(但是                     可以将图片放在css中通过背景图片的方式访问,文件大小还是个问题)

                   代码比较丑,大量的编码字符,大量的图片插入编辑器可能会卡死(当然也可以通过构建工具动态插入)

       总的来说这个得权衡使用,数量个文件大小上都需要限制,个别小的图片还可以,大的就不行了

       1.4、合并脚本和样式表

                如今的很多网站都使用了JavaScript和CSS。前端工程师必须选择是对JavaScript和CSS、进行“内联”还是外联。一般来说使用外部脚本和样式表对性能更有利。然而,如果遵循软件工程师所推荐的方式和模块化的原则将代码分开放在多个小文件中,会降低性能,因为每个文件都会导致一个额外的HTTP请求

                合并的时候需要注意合并块的组合方式,

2、使用内容发布网络

内容发布网络(CDN)是一组分布在多个不同地理位置的web服务器,用于更加有效地向用户发布内容。通常只在讨论性能问题时会提到它的性能,但它还能节省成本。在优化性能时,向特定用户发布内容的服务器的选择基于网络可用度的测量。例如,CDN可能选择网络阶跃数最小的服务器,或者具有最短响应时间的服务器

一些大型Internet公司都拥有他们自己的CDN,但使用一个CDN服务商更为有效

除了缩短响应时间之外,CDN还可以带来其他优势。他们的服务包括备份,扩展存储能力和进行缓存。

缺点:1、是你的响应时间可能会受到其它网站---甚至很可能是你竞争对手流量的影响。CDN服务器提供商在其2所有客户之间共享Web服务器组。(如果在打开自家网站的同时其它含有CDN网站的用户也在请求 就会造成请求量过多,卡顿的现象)、

2、无法直接控制组件服务器所带来的特殊麻烦。例如修改HTTP响应头必须通过服务提供商来完成,而不是由你的工作团队完成。

3、如果CDN服务商的性能下降了,你的工作质量也随之下降

CDN主要用于发布静态内容,如图片,脚本,样式表和Flash。提供动态HTML页面会引入特殊的存储需求---数据库连接,状态管理,验证,硬件和OS优化等。这些复杂性超越了CDN的能力范围。另一方面,静态文件更容易存储并具有较少的依赖性。这就是为什么对于地理上分散的用户人群来说,CDN能轻易的得到响应速度上的提高

3、添加Expires头

浏览器(和代理)使用缓存来减少HTTP请求的数量,并减小HTTP响应的大小,使Web页面加载的更快,Web服务器使用Expires头来告诉Web客户端它可以使用一个组件的当前副本到直接指定的时间为止。HTTP规范中简要地称该头为“在这一日期/时间之后响应将被认为是无效的”

Expire:Thu,15 Apr 2030 20:00:00 GMT

这是一个有效非常长久的Expires头,它告诉浏览器该响应的有效性持续到2030年4月15号为止。如果为页面中的一个图片返回了这个头,浏览器在后续的页面浏览中会使用缓存的图片,将HTTP请求的数量减少一个

Max-Age 和mod_expires

在解释缓存如何能很好地改善传输性能之前,需要提及除了Expires头之外的另一种选择。HTTP1.1引入了Cache-Control头来客服Expires头的限制。因为Expires头使用一个特定的事件,它要求服务器和客户端的时钟严格同步,另外,过期日期需要经常检查,并且一旦未来这一天到来了,还需要在服务器配置中提供一个新的日期

换一种方式,Cache-Control使用max-age指令指定组件被缓存多久。它以秒为单位定义了一个更新窗。如果从组件被请求开始过去的秒数少于max-age,浏览器就使用缓存的版本,这就避免了额外的HTTP请求,一个长久的max-age头可以将刷新窗设置为未来的10年

Cache-Control:max-age=315360000

使用带有max-age的Cache-Control可以消除Expires的限制,但对于不支持HTTP1.1的浏览器,你可能仍然希望提供Expires头,你可以同时指定这两个响应头---Expires和Cache-Control max-age。如果两者同时出现,HTTP规范规定max-age指令将重写Expires头,然而,如果你很尽职尽责,你仍然需要担心Expires带来的始终同步和配置维护问题

幸运的是,mod_expires Apache模块,使你在使用Expires头时能够像max-age那样以相对的方式设置日期,这通过Expires-Default指令来完成。在下面的例子里,图片,脚本,和样式表的过期时间被设计为自请求开始的10年之后

<FilesMatch "\.(gif|jpg|js|css)$">

ExporesDefault "access plus 10 years"

</FilesMatch>

时间可以以年、月、日、小时、分钟、秒为单位来设置。它同时向响应中发送Expires头和Cache-Control max-age头

Expores:Sun,16 Oct 2030 05:43:02 GMT

Cache-Control:max-age=315360000

实际过期日期根据何时接收到请求而变,但是即便如此,它永远都是10年以后。由于Cache-Control具有优先权并且明确指出了相对于请求时间所经过的秒数,始终同步问题就被避免了,不用担心固定日期的更新,他在HTTP1.0浏览器中也能够使用。跨浏览器你改善缓存的最佳解决方案就是使用由ExpiresDefault设置的Expires头

空缓存VS完整缓存

只有在用户已经访问过你的网站之后,长久的Expires头才会对页面浏览产生影响。当用户第一次访问你的网站时,它不会对HTTP请求的数量产生任何影响,此时浏览器的缓存是空的,因此,其性能的改进取决于用户在访问你的页面时是否有完整缓存。你的访问流量几乎都来自那些有完整缓存的用户。使你的组件可缓存能够改善这些用户的响应时间.

当我说“空缓存”或“完整缓存”时,我指的是与你的页面相关的浏览器缓存的状态。如果你的页面中的组件没有放在缓存中,则缓存为“空”。浏览器的缓存可能包含来自其它网站的组件,但这对你的页面没有帮助。反之如果你的页面中的可缓存组件都在缓存中,则缓存是“完整的”。

空缓存或完整缓存页面浏览的数量取决于Web应用程序的本质。一个类似“每日一词”的网站对典型用户来说,每个会话可能只产生一个页面浏览。很多原因会导致当用户下一次访问网站时,“每日一词”的组件可能会不在缓存中:

1、尽管他很渴望掌握更多的词语,他仍任可能只是每周或者每月访问该页面一次,而不是每天一次

2、在最近一次访问之后,用户可能手动清空了他的缓存

3、用户可能访问了太多其他网站以至缓存已满,“每日一词”的组件被移出缓存

4、浏览器或杀毒软件可能会在关闭浏览器时清空缓存

由于每次会话只产生一个页面浏览,“每日一词”的组件不太可能会被缓存,因此带有完整缓存的页面浏览所占的百分比是很低的

另一方面,在一个旅游网站或Email网站中可能每个用户会话能产生多次页面浏览,完整缓存的页面浏览器就会多一些。在这种情况下,很多页面会在浏览器缓存中发现你的组件

我们在Yahoo对此进行了测量,发现拥有完整缓存的,每天至少访问一次的唯一用户数占40%-60%,取决于Yahoo的内容。同样的研究表明带有完整缓存的页面浏览数为75%-85%。注意第一个统计的是“唯一用户”,而第二个测量的是“页面浏览”。拥有完整缓存的页面浏览所占的百分比要高于拥有完整缓存的唯一用户,因为很多Yahoo功能在每个会话中会接收到多次页面浏览。用户在一天中只会有一次使用空缓存,然后很多后续页面访问都将拥有完整缓存

这些浏览器缓存统计数据解释了为什么优化完整缓存体系是那么的重要。我们希望40%-60%的用户和75%-85%的页面浏览的完整缓存能够得到优化。这一百分比对你的网站来说有可能不同,但只要用户通常每个月至少访问你的网站一次,或每会话能产生多次页面浏览,这一统计值就会差不多。通过长久使用的Expires头可以增加被浏览器缓存的组件的数量,并在后续页面浏览中重用它们,而无需通过用户的Internet连接发送一个字节。

不仅仅是图片

为图片使用长久的Expires头非常之普遍,但这一最佳实践不应该仅限于图片。长久的Expires头应该包含任何不经常变化的组件,包括脚本、样式表和Flash组件。但是,HTML文档不应该使用长久的Expires头,因为它包含动态内容,这些内容在每次用户请求时都将被更新

理想情况下,页面中的所有组件都应该具有长久的Expires头,并且后续的页面浏览中只需为HTML文档进行一个HTTP请求。当文档中的所有组件都是从浏览器缓存中读取出来时,响应时间会减少50%或更多

修订文件名

如果我们将组件配置为可以由浏览器代理缓存,当这些组件改变时用户如何获得更新呢?当出现了Expires头时,直到过期日期为止一直会使用缓存的版本。浏览器不回检查任何更新,直到过了过期日期。这也是为什么使用Expires头能够显著地减少响应时间--浏览器直接从硬盘上读取组件而无需生成任何HTTP流量。因此,即使在服务器上更新了组件,已经访问过网站的用户也不大可能获取更新的组件(因为前一个版本已经在他们的缓存中了)

为了确保用户能够获取组件的最新版本,需要在所有HTML页面中修改组件的文件名。

最有效的解决方案是修改其所有连接,这样,全新的请求从原始服务器下载最新的内容

取决于你如何构造HTML页面,这项工作可能很轻松也可能很痛苦。如果你使用PHP。Perl等动态语言生成HTML页面,一种简单的解决方案就是为所有组件的文件名使用变量。使用这种方法,在页面中更新文件名只需要在某个地方修改变量。在Yahoo!我们经常将这一步作为生成过程的一部分---将版本号嵌在组件的文件名中(例如 yahoo_2.0.6.js),而且在全局映射中修订过的文件名会自动更新。嵌入版本号不仅可以改变文件名,还能在调试时更容易地找到准确的源代码文件。

添加长久的Expires头可以将后续页面浏览的响应时间从600毫秒降低到260毫秒,这是在900Kbps的DSL上测试的,减少了57%。页面中的组件越多,影响时间改善得越多。如果你的页面平均超过6个图片,3个脚本和一个样式表,页面的速度提升就会超过这个例子的57%

节省下来的这些事件究竟是从哪里来的呢?我在前面提到过,一个具有长久Expires头的组件将会被缓存,在后续请求时浏览器直接从硬盘上读取它,避免了一个HTTP请求。然而,我并没有介绍相反的情况。如果一个组件没有长久的Expires头,他仍然会存储在浏览器的缓存中。在后续请求中,浏览器会检查缓存并发现组件已经过期(HTTP术语称之为“陈旧”)。为了提高效率,浏览器会向原始服务器发送一个GET请求。如果组件没有改变,原始服务器可以免于发送整个组件,而是发送一个很小的头,告诉浏览器可以使用其缓存的组件

这些条件请求加起来,就是节省的时间。很多时候,正如我们为您在十大网站中看到的那样,组件并没有更改,而浏览器总是从磁盘上读取它们。通过使用Expires头来避免额外的HTTP请求,可以减少一半的响应时间。

4、压缩组件

前端工程师作出的决定可以显著地减少网络上传送HTTP请求和响应所花的时间。的确,用户地宽度速度,Internet服务提供商,对等交换点的距离和其他因素超出了开发团队的控制范围。然而,仍然有很多变数可以影响响应时间,规则1和规则3通过限制不必要的HTTP请求解决了响应时间的问题。如果没有HTTP请求--理想情况下--也就没有了网络活动,规则2通过将HTTP响应拉近用户来减少响应时间

本章介绍的规则4通过减小HTTP响应的大小来减少响应时间。如果HTTP请求产生的响应包越小,传输时间就会减少,因为只需要将很小的包从服务器传递到客户端。这一效果对速度较慢的宽带尤其明显。本章展示了如何使用gzip编码来压缩HTTP响应包,并由此减少网络响应时间。这是减小页面大小的最简单的技术。但影响是最大的,还有很多的方式可以减小HTML文档的页面大小(删除注释和缩短URL等),但它们需要更多的工作,且收益甚微

压缩是如何工作的

用于减小文件体积的文件压缩已经在Email应用和FTP站点中使用了10年以上,同样的技术也可以用于向浏览器发布压缩的Web页面。从HTTP1.1开始,Web客户端可以通过HTTP请求中的Accept-Encoding头来标识对压缩的支持

Accept-Encoding:gzip,deflate

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

Content-Encoding:gzip

gzip是目前最流行和最有效的压缩方法。这是GNU项目开发的一种免费的格式(也就是说在专利和其他限制方面没有任何阻碍)并被标准化为RFC 1952 你可能见到的另一种压缩格式是deflate,但其效果略逊且不太流行,事实上,我只见过一个使用deflate的网站---msn.com。支持deflate的浏览器也支持gzip,但很多浏览器支持gzip却不支持deflate,因此gzip是最理想的压缩方法。

压缩什么

服务器是基于文件类型选择压缩什么,但这通常受限于对其进行的配置。很多网站会压缩其HTML文档。压缩脚本和样式表也是非常值得的,但很多网站没有这样做(实际上,值得压缩的内容包括XML和JSON在内的任何文本响应,但这里只关注脚本和样式表,因为它们用的最普遍)。图片和PDF不应该压缩,因为它们本来就已经被压缩了。试图对它们进行压缩只会浪费CPU资源,还有可能会增加文件大小

压缩的成本有---服务器端会花费额外的CPU周期来完成压缩,客户端要对压缩文件进行解压缩。要检测收益是否大于开销,需要考虑响应的大小,连接的宽带和客户端与服务器之间的Internet距离。这些信息通常难以得到,即便得到了,也有其它变数需要考虑,根据经验通常大于1KB或者2KB的文件进行压缩。mod_gzip_minimum_file_size指令控制着希望压缩的文件的最小值,默认值是500B。

我观察了美国10个最流行的网站的gzip使用情况。9个网站压缩其HTML文档,7个网站压缩了大多数脚本和样式表,只有5个网站压缩了所有的脚本和样式表。没有压缩任何HTML文档、样式表和脚本的网站错过了将其页面减少70%以上的机会,这就在下一节“节省”中介绍

节省

压缩通常能将响应的数据量减少将近70%。表4-2列出了脚本和样式表(有大有小)的压缩示例,使用deflate的结果也显示了出来

文件类型             未压缩大小    gzip大小     gzip节省  deflate大小    deflate节省

脚本                    3277字节       1076字节   67%         1112字节        66%

脚本                    49713字节     14488字节  64%        16583字节     58%

样式表                968字节          426字节     56%         463字节        52%

样式表                14122字节      3748字节   73%         4665字节     67%

从表4-2中可以清楚的看到gzip是典型的压缩选择。gzip能将响应整体尖啸6%,而deflate能减小60%。对于这些文件,gzip能比deflate多压缩6%

配置

配置gzip时使用的模块取决于Apache的版本---Apache1.3使用mod_gzip而Apche2.x使用mod_deflate。这一节极少如何配置每个模块,但仅介绍Apache,因为它是Internet上最流行的Web服务器

Apache 1.3 ---mod_gzip

Apache 1.3 的gzip压缩是由mod_gzip模块提供,有很多mod_gzip配置指令,在mod_gzip的网站上有所描述,以下是最常用的指令

mod_gzip_on 启用mod_gzip

mod_gzip_item_include

mod_gzip_item_exclude

基于文件类型、MIME类型、用户代理等帝国一哪些需要压缩,哪些不需要需要压缩,哪些不需要

很多Web主机服务器都默认为text/html打开了mod_gzip。你要做的最重要的配置修改就是需要明确压缩脚本和样式表

gzip命令行工具提供了一个选项,用于控制压缩的程度,可以在CPU使用量和数据大小的变化之间进行取舍,但mod_gzip中没有配置指令能够控制压缩级别。如果流式压缩产生的CPU负载成问题,可以考虑在批判或者内存中缓存经过压缩的组件。手工压缩响应和更新增加了你的维护工作,并可能成为一种负担。幸运的是,mod_gzip提供了选项,可以将保存压缩过的内容自动保存在磁盘上,并在原内容发生变化时更新压缩过的内容。使用mod_gzip_can_negotiate和mod_gzip_update_static指令可以完成这一任务

代理缓存

当浏览器直接与服务器通信时,迄今为止所介绍的配置都能很好地工作。Web服务器基于Accept-Encoding来检测是否对响应进行压缩。不管是否压缩过,浏览器都会基于响应中地其他HTTP头如Expires和Cache-Control来缓存响应

当浏览器通过代理来发送请求时,情况就变得复杂了。假设针对某个URL发送到代理的第一个请求来自于一个不支持gzip的浏览器。这是到达代理的第一个请求,因此其缓存为空,代理会将请求转发到Web服务器。此时服务器的响应是未经过压缩的。这个没有压缩的响应被代理缓存起来并发送给浏览器,现在,假设到达代理的第二个请求访问的是同一个URL,来自于一个支持gzip的浏览器。代理会使用其缓存中(未经压缩)的内容进行响应,这就失去了进行压缩的机会,如果顺序反了---第一个请求来自于一个支持gzip的浏览器,而第二个请求来自于一个不支持gzip的浏览器---情况可能更严重。在这种情况下,代理的缓存中拥有内容的一个压缩版本,并将这个版本提供给后续的浏览器,而不管它们是否支持gzip。

解决这一问题的办法就是在Web服务器的响应中添加Vary头。Web服务器可以告诉代理根据一个或多个请求头来改变缓存的响应。由于压缩的决定是基于Accept-Encoding请求头的,因此需要在服务器的Vary响应头中包含Accept-Encoding

Vary:Accept-Encoding

这将使得代理缓存响应多个版本,为Accept-Encoding请求头的每个值缓存一份。在前面的例子中,代理会缓存每个响应的两个版本---Accept-Encoding为gzip时的压缩内容和没有指定Accept-Encoding时的非压缩内容。当浏览器带着Accept-Encoding:gzip访问代理时,它接收到的是压缩过的内容。没有Accept-Encoding请求头给他浏览器收到的是未压缩的内容。默认情况下,mod_gzip会向所有响应添加Vary:Accept Encoding 头,以驱使代理执行正确的操作

边缘情况

服务器和客户端的压缩对等看似简单,但必须正确才行。无论是客户端还是服务器发生错误(发送压缩内容到不支持它的客户端,忘记将压缩内容声明为已经进行了gzip编码等),页面都会被破坏。错误并不会经常发生,但他们是必须考虑的边缘情况

今天大约90%的透过浏览器进行的Internet通信都需要使用gzip。如果一个浏览器宣称支持gzip,你通常可以相信它。但Internet Explorer 早期末打补丁的版本存在着一些已知的缺陷,尤其是IE5.5和IE6.0SP1。还有其他一些问题,但这些问题只发生在Internet通信量1%的浏览器上。一种安全的方式是只为已经证实过支持压缩的浏览器提供压缩内容。如IE5以及IE6之后的版本。这被称为浏览器白名单方式

使用这种方式,可能会失去为一小部分本来支持压缩的浏览器提供压缩的机会。但其他选择---为不支持压缩的浏览器提供压缩内容更加糟糕。在Apache1.3的mod_gzip中,可以通过在mod_gzip_item_include中使用恰当的User-Agent值来指定浏览器白名单

mod_gzip_item_include regheader "User-Agent :MSIE[6-9]"

mod_gzip_item_include regheader "User-Agent :Mozilla/[5-9]"

把代理缓存加进来后,处理这些边缘情形浏览器将变得更为负责。你不可能和代理共享浏览器白名单配置。用于设置浏览器白名单的指令过于复杂,无法使用HTTP头进行编码。最佳做法是将User-Agent作为代理的另外一种评判标准添加到Vary头中

Vary:Accept-Encoding,User-Agent

当mod_gzip检测到你在使用浏览器白名单时,它会自动将User-Agent字段添加到Vary头。不幸的是,User-Agent有上千种不同的值。代理不太可能为其所有的代理的所有URL缓存Accept-Encoding和User-Agent的全部组合。mod_gzip文档甚至说:“使用对User-Agent HTTP头进行求职的过滤器规则将会导致完全禁用为响应包进行的缓存。”因为这实际上破坏了代理缓存。另外一种方式是使用Vary:*或Cache-Control:private头来禁用代理缓存。因为Vary:*头放置了浏览器使用缓存的组件,最好使用Cache-Control:private,Google和Yahoo都使用了这种方式,记住这是为所有浏览器禁用代理缓存,因此会增加你的宽带开销,因为代理无法缓存你的内容

如何平衡压缩和代理支持的决定是很复杂的,需要在加快响应时间,减小宽带开销和边缘情况浏览器缺陷之间进行权衡。正确的答案取决于你的网站

1、如果你的网站用户很少,并且他们处在一个小圈子中(例如,他们在一个intranet中,或者都使用Firefox1.5),边缘清醒浏览器就不需要太多关注。可以压缩内容并使用Vary:Accept-Encoding。这样可以通过减小组件的大小和利用代理缓存来改善用户体验

2、如果你更注意宽带开销,可以和前一种情况一样--压缩内容并使用Vary:Accept-Encoding。这降低了服务器端的宽带开销并提升了代理处理的请求数量

3、如果你拥有大量的,多变的用户群,能够应付较高的宽带开销,并且享有高质量的名声,请压缩内容并使用Cache-Control:private。这禁用了代理但避免了边缘情形缺陷

规则5--将样式表放在顶部

Yahoo的一个负责主要门户的团队向他们的页面中添加了大量DHTML特性,并尝试确保他们不会对响应时间产生负面影响。其中一个复杂的DHTML特征是在发送Email消息时弹出一个Div,这不属于实际呈现页面的一部分--它只在页面加载完毕后,用户单击按钮来发送Email消息时才会访问。由于在呈现页面时不需要使用它,前端工程师将弹出式div所需的Css放在了外部样式表中,并在页面的底部添加了相应的link标签,期望将其包含在页面的末尾能够使其加载更快

这背后的逻辑是有意义的。其他很多组件(图片,样式表,脚本,等等)是呈现页面所必须的。由于组件(通常)是按照他们在文档中出现的顺序下载的,将有DHTML特性的样式表放在最后可以使得很多重要的组件首先被下载,从而得到一个加载很快的页面。

果真如此吗?

在Internet Explorer(仍然是最流行的浏览器)中,实际产生的页面比原来的明显缓慢。在尝试寻找一种能为页面加速的方法时,我们发现将DHTML特征的样式表放在文档顶部--Head中---能使页面加载的更快。这与我们所期望的相矛盾。将样式表放在前面,会延迟页面中其它重要组件的加载,怎么会改善页面加载时间呢?对此更深入法人研究导致了规则5 的出现

逐步呈现

关心性能的前端工程师都希望页面能够逐步加载,也就是说,我们希望浏览器能够尽快显示内容。这对于很多内容的页面以及Internet连接很慢的用户来说尤其重要。为用户提供可视化回馈的重要性已经有了很好的研究和记载。

进度指示器有三个主要优势--它们让用户直到系统没有崩溃,只是正在为他解决问题;它们指出了用户大概还需要等多久,以便用户能够在漫长的等待中做些其他事情;最后,它们能够给用户提供一些可以看的东西,使得等待不再是那么无聊。最后一点优势不可低估,这也是为什么推荐使用图形进度条而不是仅仅以数字形式显示预期的剩余时间

在我们这里,HTML页面就是进度指示器。当浏览器逐步地加载页面时,页头,导航栏。顶端logo等,所有这些部门都会为等待页面的用户提供视觉反馈。这改善了整体用户体验

将样式表放在文档底部会导致在浏览器中阻止内容的逐步呈现。为避免当样式变化时重绘页面中的元素,浏览器会阻塞内容逐步呈现。规则5对于加载页面所需的实际时间没有太多影响,它影响更多的是浏览器对这些组件顺序的反应,实际上,用户感觉缓慢的页面反而是可视化组件加载的更快的页面。在浏览器和用户等待位于底部的样式表时,浏览器会延迟显示任何可视化组件。下一节给出的示例展示了这一现象,我将其称之为“白屏”

白屏

将样式表放在文档底部时如何延迟页面加载的。这个问题很难追踪到,因为它只发生在Internet Explorer中,并且依赖于页面是如何加载的。在使用过这个页面之后,你会发现偶然发生页面加载缓慢。当发生这种现象时,页面会完全空白,直到页面所有的内容同事涌上屏幕。这是一种不好的用户体验,因为没法向用户确保他的请求正在被正确的处理,用户会因为不知道发生了什么而离开。这就是用户离开你网站投奔竞争对手的原因

在Internet Explorer中,将样式表放在文档底部会导致白屏问题的情形有以下几种:

在新窗口中打开时

单击示例页面中的“new window”连接,在新窗口中打开“Css at the Bottom”。用户通常在跨站导航时打开新窗口,如从搜索结果页导航到实际的目的页

重新加载时

单击刷新按钮时另外一种导致白屏的方式,这是一种常见的用户操作。在页面加载时最小化然后恢复窗口就能看到白屏

作为主页

将浏览器默认页面设置为 自己网站的首页并打开新的浏览器窗口就会导致白屏。规则5 对于那些希望其网站能被用作主页的团队来说尤其重要。

将CSS放在顶部

为了避免白屏,请将样式表放在文档顶部的Head中。经过这样修改后的示例网站称作为“CSS at the Top”,解决了所有错误情况。不管页面是如何加载的---在新窗口打开,重新加载者作为主页---页面都是逐步呈现的

将样式表包含在文档中有两种方式:使用LINK标签和@import规则。

一个Style块可以包含多个@import规则,但@import规则必须放在所有其他规则之前。我曾遇到过没有注意到这一点的情形,开发人员需要花时间去尝试检查为什么@import规则中的样式表没有加载。出于这一原因,我更喜欢使用LINK标签(需要了解的东西较少)。除了语法更简单外,使用LINK标签来代替@import还能带来性能上的收益。@import规则有可能会导致白屏现象,即使把@import规则放在文档的Head标签中也是如此

使用@import规则会导致组件下载时的无序性。

无样式内容的闪烁

白屏现象源自于浏览器的行为。要知道我们的样式表在呈现页面时几乎用不到--它只影响发送Email消息时的DHTML特性。尽管Internet Explorer已经得到了所需的组件,它依然要等到样式表下载完毕之后再呈现它们,样式表在页面中的为止并不影响下载时间,但是会影响页面的呈现。

如果样式表仍在加载,构建呈现树就是一种浪费,因为在所有样式表加载并解析完毕之前无需绘制任何东西。否则,在其准备好之前显示内容会遇到FOUC(无样式内容的闪烁,Flash of Unstyled Content)问题

白屏是浏览器在尝试修改前端工程师所犯的错误---将样式表放在文档比较靠后的文职。白屏是对FOUC问题的弥补。浏览器可以延迟呈现,知道所有的样式表都下载完之后,这就导致了白屏。反之,浏览器就可以逐步呈现,但要承担闪烁的风险。这里没有完美的选择

前端工程师应该做什么

那么如何同时避免白屏和无样式内容的闪烁呢?

在“无样式内容的CSS闪烁”示例中,闪烁并不总是发生,它取决于你的浏览器以及如何加载页面。在本章前面的内容中,我曾介绍过白屏仅当在新窗口中加载页面、重新加载和作为主页时在Internet Explorer中发生。在这些情况下,Internet Explorer选择第二种方式--承担FOUC风险

Firefox则是一致的---它总是选择人第二种方式(FOUC)。无论任何情况在Firefox中的行为都是一样的--它们会逐步呈现。对于第一个例子,Firefox的行为更考虑用户感受,因为样式表对于呈现页面来说并不是必须的,但在“无样式内容的CSS闪烁”示例中,用户就不那么幸运了。用户会切实体验到FOUC问题,因为Firefox是逐步呈现的

当浏览器的行为不同时,前端工程师应该做些什么呢?

由于历史原因,浏览器支持违反HTML规范的页面,这是为了让那些老旧的,不规整的页面也能够浏览,但在处理样式表方面,Internet Explorer和Firefox都要求Web开发社区遵循规范。这就导致即使要承担降低用户体验的风险,违反了规范的页面(将Link放到HEAD节的外面)忍让能够呈现

在努力改善Web上最频繁访问的页面时,Yahoo!的门户团队最初将样式表放到了页面底部,结果适得其反。它们发现最佳解决方案就是遵循HTML规范,将它放在顶部。两种情况--白屏和无样式内容的闪烁--都不在是风险。如果你的样式表不要求呈现页面,可以想办法在文档加载完毕后加载进来,如第8章中“加载后下载”一节所述,否则否则不管你的样式表在呈现页面时是否必须,都应该遵循这个规则

规则6--将脚本放在底部

第5章介绍了将样式表放在页面的底部会阻碍页面逐步呈现,以及如何通过将其移至文档的HEAD中来解决这一问题。脚本(外部JavaScript文件)会引起类似的问题,但解决方案恰好相反---最好将脚本从页面的顶部移到底部(如果可以的话)。这样页面既可以逐步呈现,也可以提高下载的并行度。

脚本带来的问题

将脚本放在中部

经过编译的脚本下载需要10秒钟,因此很容易看到问题---页面的下半部分要花大约10秒才能显示出来。出现这一现象是因为脚本阻塞了并行下载。

示例页面的另一个问题是逐步呈现。再使用样式表时,页面逐步呈现会被阻止,知道左右的样式表下载完成。这就是最好将样式表移到文档的HEAD中的原因,这样就能首先下载它们而不会阻止页面呈现。使用脚本时,对于所有位于脚本以下的内容。逐步呈现都被阻塞了,将脚本放在页面越靠下的地方,意味着越多的内容能够逐步呈现

并行下载

对响应时间影响最大的是页面中组件的数量。当缓存为空时,每个组件都会产生一个HTTP请求,有时即使缓存是完整的亦是如此。要知道浏览器会并行地执行HTTP请求,你可能会问,为什么HTTP请求的数量会影响响应时间呢?浏览器不能一次将它们都下载下来吗?

对此的解释要回到HTTP1.1规范,该规范建议浏览器从每个主机名并行地下载两个组件。很多Web页面需要从一个主机名下载所有的组件。查看这些HTTP请求会发现它们是呈阶梯状的。

如果一个Web页面平均地将其组件分别放在两个主机名下,整体响应时间将可以减少大约一半。HTTP请求的行为的并行数量会增加,每个组件的下载时间是不变的 但是整体的时间缩短了大约一半、

每个主机名并行下载两个组件的限制只是一个建议。默认情况下,Internet Explorer和Firefox都遵守这一建议,但用户可以重写该默认设置。Internet Explorer将这个值存放在Registry Editor中

在Firefox中,可以使用about:config页面中的network.http.max-persistent-connections-per-server设置来修改这一默认设置。有意思的是,对于HTTP1.0,Firefox的默认值是每个主机名并行下载8个组件,下载效果更好,今天的很多网站使用HTTP1.1,但将并行下载数增加到每个主机名超过两个也是有可能的。前端工程师与其依赖用户来修改浏览器设置,不如简单地使用CNAME(DNS别名)来将组件分别放到多个主机名中。但增加并行下载数量并不是没有开销的,其优劣取决于你的宽带和CPU速度,过多的并行下载反而会降低性能。Yahoo!的研究表明,使用两个主机民比使用1、4或10个主机名带来更好的性能

脚本阻塞下载

并行下载组件的优点是很明显的。然而,在下载脚本时并行下载实际上是被禁用的--即使使用了不同的主机名,浏览器也不会启动其它的下载。其中一个原因是,脚本可能使用document.write来修改页面内容,因此浏览器会等待,以确保能够恰当地布局。

在下载脚本时浏览器阻塞并行下载的另外一个原因是为了保证脚本能够按照正确的顺序执行。如果并行下载多个脚本,就无法保证响应是按照特定顺序到达浏览器的,例如,后面的脚本比页面中之前出现的脚本更小,它可能首先执行。如果它们之间存在着依赖关系,不按顺序执行就会导致JavaScript错误。下载JavaScript的时候不会并行下载其它的组件

最差情况:将脚本放在顶部

至此,脚本对Web页面的影响就清楚了

1、脚本会阻塞对其后面内容的呈现

2、脚本会阻塞对其后面组件的下载

如果将脚本放在页面顶部---正如通常的情况那样---页面中的所有东西都位于脚本之后,整个页面的呈现和下载都会被阻塞,直到脚本加载完毕

由于整个页面的呈现被阻塞,因此导致了第5章所介绍的白屏现象。逐步呈现对于良好的用户体验来说是非常重要的,单缓慢的脚本下载延迟了用户所期待的反馈。此外,由于并行下载数的减少,不管图片显示有多块,都要被延迟。

最佳情况

放置脚本的最好地方是页面的底部这不会阻止页面内容的呈现,而且页面中的可视组件可以尽早下载。在将脚本放在底部后,虽然其请求时间较长,但对页面的影响很小。

正确的放置

前面那些示例是使用了需要10秒才能下载完的脚本。希望你使用的脚本不需要这么长时间的延迟,但是一个脚本很可能花费比预期长的时间,用户的宽带也会影响脚本的响应时间。你的页面中的脚本所产生影响可能没有这里展示的这么严重,但仍需要注意。在页面中包含多个脚本也会带来问题

在很多情况下,很难将脚本移到底部,例如,如果脚本使用document.write向页面中插入了内容,就不能将其移动到页面中靠后的位置。此外还有作用域的问题。很多情况下。可以使用其它办法解决这些情形

经常出现的另一种建议是使用延迟(Defferred)脚本。DEFER属性表明脚本不包含document.write,浏览器得到这一线索就可以继续呈现

不幸的是,在Firefox中,即便是延迟脚本也会阻塞呈现和并行下载。在InternetExplorer中,页面中靠后的组件下载的明显要晚一些。如果一个脚本可以延迟,那么它一定可以移到页面的底部。这是加速Web页面的最佳方式

规则7---避免CSS表达式

CSS表达式是动态设置CSS属性的一种强大(并且危险)的方式,它受到Internet Explorer版本5和之后版本的支持。我们从一个传统的设置背景色的CSS规则开始----

background-color:#BBD4FF

对于很多动态页面,可以使用CSS表达式将背景色设置为每小时变化一次

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

正如这里所示,expression方法加收一个JavaScript表达式。CSS属性将设置为对JavaScript表达式进行求值的结果。

expression方法被其它浏览器简单地忽略了,但是对于IE而言这是一种有用的工具,能够在Internet Explorer中设置属性,创建跨浏览器的一直体验。例如,Internet Explorer5不支持min-width属性。CSS则是解决这一问题的一种方法。下面的示例确保一个页面的宽度最少600px,Internet Explorer 可以识别表达式,而其他浏览器识别静态设置

width:expression(document.body.clientWidth<600 ? "600px" : "auto")

min-width:600px

大多数浏览器会忽略width属性而使用min-width属性,因为它们不支持CSS表达式。Internet Explorer则忽略min-width,而根据文档的宽度动态地设置width属性。当页面变化时,CSS表达式会重新求值,如大小变化时。这确保了每当用户改变其浏览器大小时,宽度都能恰当的调整,对CSS表达式的频繁求值使其得以工作,但也导致CSS表达式的低下性能

更新表达式

表达式的问题在于对其进行的求值频率比人们期望的要高。它们不只在页面呈现和大小改变时求值,当页面滚动,甚至用户鼠标在页面上移过时都要求值。我们可以向CSS表达式中添加一个计数器来进行跟踪,看看CSS表达式何时被求值,以及有多么频繁

p{

width:expression(setCntr(),document.body.clientWidth<600 ? "600px" : ''auto");

min-width:600px;

border:1px solid

}

setCntr()函数增加一个全局变量的值并将这个值写到页面中的一个文本框里。页面中有10个段落。加载页面会执行CSS表达式40次。这之后,对于这种事件,如改变大小、滚动和鼠标移动,该CSS表达式都会被求值10次。在页面上来回移动鼠标可以很轻易的产生1000次以上的求值。在这个例子里CSS的危险显而易见。最郁闷的是,在Internet Explorer中单击这个页面中的文本框之后,你就不得不终止这个进程了。

围绕问题展开工作

很多CSS专家都非常熟悉CSS表达式,也直到如何避免前面的例子中显现出来的缺陷。有两种技术可以避免CSS表达式产生这一问题--创建一次性表达式和使用事件处理器取代CSS表达式

一次性表达式

如果CSS表达式必须被被求值一次,那么可以在这一次执行中重写它自身。本章开始时定义的北京样式就非常适合使用这种方式

<style>

p{

background-color:express(altBgcolor(this))

}

</style>

<script type="text/javascript">

function altBgcolor(elem){

elem.style.backgroundColor=(new Date()).getHours()%2 ? "#F08A00" : "#B8D4FF"

}

</script>

CSS表达式调用了altBgcolor()函数,而该函数将样式的background-color属性设置为一个明确的值,并移除了CSS表达式。这个样式被关联到页面中的10个段落。无论再改变大小,滚动或页面上移动鼠标之后,CSS表达式都只会执行10次,这比之前一个例子中的上万次要好的多

事件处理器

我看到过的绝大多数使用CSS表达式的情形,都可以找到不需要CSS表达式的替代方法。CSS表达式从自动绑定到浏览器事件中获益,但这也是它的缺陷。除了使用CSS表达式之外,前端工程师还可以尝试使用事件处理器来为特定的事件提供所期望的动态行为。这就避免了在无关事件发生时对表达式的求值。下面的事件处理器示例展示了通过在onresize事件中设置样式的width属性来修正min-width问题,避免了鼠标移动、滚动等事件产生的成千上万次不必要的求值

事件处理器的示例

当浏览器的大小改变时,这个例子使用setMinWidth()函数来修改所有段落元素的大小

function setMinWidth(){

setCntr()

var aElements = document.getElementByTagName("p")

for(var i=0;i<aElements.length;i++){

aElements[i].runtimeStyle.width = (document.body.clientWidth < 600 ? "600px" : "auto")

}

}

if(l!==navifator.userAgent.indexOf("MSIE")){

window.onresize = setMinWidth

}

这会在浏览器大小改变时动态地设置宽度,但在第一次呈现时这并不能恰当地设置段落地大小。因此,页面还需要使用“一次性表达式”一节中介绍地方法,通过CSS表达式设置初始宽度,并在第一次求值后重写CSS表达式

小结

有些规则用于处理页面加载之后地性能问题,本章所介绍的只是其中之一,这通常是由CSS表达式引起的问题。然而,有的时候,CSS表达式也会影响页面的加载事件,YaHoo!的一项功能使用了CSS表达式,导致页面首次呈现事件延迟了20秒。这个结果不是预期的,而是需要花一些事件进行诊断。类似的,谁会想到用户单击了一个文本框会导致Internet Explorer锁死呢?关于复杂的CSS不兼容性--如min-width和location:fixed--的完整讨论超出了本书的范围,但可以明确的是---在没有深入了解底层影响的情况下使用CSS表达式是很危险的

规则8---使用外部JavaScript和CSS

本书介绍的很多性能规则用于处理如何管理外部组件,如通过CDN提供外部组件(规则2),确保其拥有长久的Expires头(规则3)以及压缩其内容(规则4)。然而,在出现这种考虑之前,我们应该问一个更基本的问题---JavaScript和CSS是应该包含在外部文件中还是内联在页面中?正如我们将要看到的那样,通常使用外部文件更好

内联VS外置

我们首先来对比以下内联JavaScript和CSS与将其外置之间的区别

纯粹而言,内联快一些

内联示例中只有一个HTML文档,其大小为87KB,所有的JavaScript和CSS都包含在HTML文件自身中。外部示例包含一个HTML文档(7KB),一个样式表(59KB)和三个脚本(1KB,11KB和9KB),总计87KB。尽管所需下载的总数据量是相同的,内联示例还是比外部示例快了30%-50%,这主要是因为外部示例需要承担多个HTTP请求带来的开销(第一章介绍了减少HTTP请求数量的重要性)。尽管外部示例可以从样式表和脚本的并行下载中获益,但一个HTTP请求与五个HTTP请求之间的差距导致内联示例更快一些

尽管结果如此,现实中还是使用外部文件会产生较快的页面。这是由于本例中没有涉及外部文件所带来的收益--JavaScript和CSS文件有机会被浏览器缓存起来。HTML文档--至少是那些包含动态内容的HTML文档--通常不会被配置为可以进行缓存。当遇到这种情况时(HTML文档没有被缓存),每次请求HTTP文档都要下载内联的JavaScript和CSS。另一方面,如果JavaScript和CSS是外部文件,浏览器就能缓存它们,HTML文档的大小减小,而且不会增加HTTP请求的数量

关键因素是,与HTML文档请求数量相关的,外部JavaScript和CSS组件被缓存的频率。这个因素尽管难以量化,但可以通过下面的手段进行衡量

页面查看

每个用户产生的页面查看越少,内联JavaScript和CSS的论据越强。想象一个普通用户每个月只访问你的网站一次。在每次访问之间,外部JavaScript和CSS文件很有可能从浏览器的缓存中移除,即使该组件有一个长久的Expires头

另一方便,如果普通用户能够产生很多的页面查看,浏览器很可能将(具有长久的Expires头的)外部文件放在其缓存中。使用外部文件提供JavaScript和CSS带来的收益会随着每用户每月的页面查看次数或每用户每会话产生的页面查看次数的增长而增加

空缓存VS完整缓存

在比较内联和外部文件时,直到用户缓存外部组件的可能性这一点非常重要。我们在Yahoo!进行了测量,发现每天至少携带完整缓存访问Yahoo!功能一次的用户占40%-60%。同样的研究表明,具有完整缓存的页面查看数量占75——85%。注意第一个统计测量的是“唯一用户”而第二个是“页面查看”。具有完整缓存的页面查看所占的百分比比接待完整缓存的唯一用户的百分比高,这是因为很多用户在一次会话中进行了多次页面查看。每天,用户可能只有开始的一次访问携带的是空缓存,之后的多次后续页面查看都具有完整缓存。

这些度量结果随着网站的类型而变。知道这些统计值有助于评估使用外部文件相对于使用内联带来的潜在收益。如果你的网站的本质上能够为用户带来高完整缓存率,使用外部文件的收益就更大。如果不大可能产生完整缓存,则内联是更好的选择

组件重用

如果你的网站中的每个页面都使用了相同的JavaScript和CSS,使用外部文件可以提高这些组件的重用率。在这种情况下使用外部文件更加具有优势,因为当用户在页面间导航时,JavaScript和CSS组件已经位于浏览器的缓存中了

相反的情况也很容易理解--如果没有任何两个页面共享相同的JavaScript和CSS,重用率就会非常低。难的是绝大说网站不是非黑即白的。这就带来一个单据相关的问题---当把JavaScript和CSS打包到外部文件中时,应该把边界划在哪里?

争论在文件越少越好这个前提下展开。在典型情况下,页面之间JavaScript和CSS的重用即不可能100%重叠,也不可能100%无关。在这种中间情形中,一个极端就是为每个页面提供一组分离的外部文件。这种方式的缺点在于,每个页面都强制用户使用另外一组件并产生令响应事件变慢的HTTP请求。这种方式对于普通用户只访问一个页面和很少进行跨页访问的网站来说是有意义的

如果你的网站并不符合这两种极端情况,最好的答案就是这种。将你的页面换份为几种页面类型,然后为每种类型创建单独的脚本和样式表。这比维护一个单独的文件要复杂,但通常比为每个页面维护不同的脚本和样式表要容易的多,并且对于给定的任意页面都只需要下载很少的多余的JavaScript和CSS

最后你作出的与JavaScript和CSS外部文件的边界相关的决定影响着组件的重用程度。如果你可以找到一个平衡点,实现较高的重用度,则将JavaScript和CSS部署到外部文件的论据更加强势一些。如果冲拥堵很低,还是内联更有意义以下

典型的对比结果

在对内联和使用外部文件进行对比分析时,关键点在与HTML文档请求数量相关的、外部JavaScript和CSS组件被缓存的频率。在前一节里,我介绍了三种基准(页面查看、空缓存VS完整缓存和组件复用),这有著你确定最好的选择。对于任何网站来说,正确答案都依赖于这些基准

很多网站都处于这些基准的中间位置。它们拥有每用户每月5~15次页面查看,并有每用户每会话2~5次页面查看。空缓存浏览的情况和Yahoo!类似---每天有40%~60%的唯一用户具有完整缓存,每天有75%~85%的页面查看是在完整缓存下进行的。页面之间有着恰当数量的JavaScript和CSS重用,使得少数几个文件即可覆盖每一种主要的页面类型

对于具备这样的度量结果的网站,最好的解决方案通常是将JavaScript和CSS部署到外部文件中。下面的示例演示了这一点,其中的外部组件可以被浏览器缓存。反复加载这一页面,并于第一个示例“内联JS和CSS”的结果进行比较,可以看出使用带有长久Expires头的外部文件是最快的方式

主页

我所见过的使用内联方式反而更好的一个例外是主页。主页的选择作为浏览器默认页的URL,我们从主页的角度看一下这三个基准

页面查看

主页拥有每月很高的页面查看数量。确切的说,只要浏览器打开,主页就被访问了。然而每个会话只有一个页面查看

空缓存VS完整缓存

完整缓存的百分比要比其它网站更低。处于安全的原因,很多用户选择在每次关闭浏览器时清空缓存。下一次用户打开浏览器时,产生的是一个到主页的空缓存页面查看

组件重用

重用率很低。很多主页是用户来到网站后访问的唯一一个页面,因为它们谈不上重用

分析了这些基准之后,我们更加倾向使用内联而不是外部文件。主页倾向于内联还有很多因素--它们对响应能力有着更高的要求,即便是在空缓存场景中。如果一个公司决定启动一项战略,鼓励用户将该公司网站设置为它们的主页,这些公司最不愿看到的就是一个缓慢的主页。为了公司的主页战略能够成功,页面必须很快

没有适用于所有主页的唯一答案。但在讨论到主页时必须评估这里给出的因素。如果正确答案是内联,则你可以在下一节中找到有用信息,下一节介绍了两种技术,能够利用外部文件得到内联的效益(如果可能的话)

两全其美

即便所有的因素都偏向于内联,将所有JavaScipt和CSS都添加到页面中还是会感觉很低效,而且无法利用浏览器缓存。这里介绍的两项技术使你既可以获得内联的优势,同时也能缓存外部文件

加载后下载

有一些主页--如Yahoo!主页和My Yahoo!--通常每会话只有一个页面查看。然而,并不是左右主页都这样。Yahoo! Mail就是一个很好的例子,其主页通常伴随着后续页面查看(在初始页之后,还要访问其他页面,如查看或编写Email消息)

对于作为多次页面查看中的第一次的主页,我们希望为主页内联JavaScript和CSS,但又能为后续页面查看提供外部文件。这可以通过在主页加载完成后动态下载外部组件来实现(通过onload事件)。这能够将外部文件放在浏览器的缓存中以便于用户接下来的访问其他页面

“加载后下载”示例中的JavaScript代码将doOnload函数关联到文档的onload事件。在1秒的延迟之后(确保页面呈现完毕),就会下载所需的JavaScript和CSS文件。这通过创建对应的DOM元素(分别是script和link)并赋予指定的URL来实现

在这些页面中,JavaScript和CSS被加载到页面中两次(先是内联的,然后是外部的)。要使其能够工作,必须处理双重定义。例如脚本,可以定义但不能执行任何函数(至少不能让用户察觉)。使用了相对单位(百分比或em)的CSS如果指定两次可能会产生问题,将这些组件放到一个不可见的IFrame中是一种更好的方式,能够避免这些问题

动态内联

如果主页服务器知道一个组件是否在浏览器的缓存中,它可以在内联或使用外部文件之间做出最佳的选择。尽管服务器不能查看浏览器缓存中有些什么,但可以用cookie做指示器。如果cookie不存在,就内联JavaScript和CSS。如果cookie出现了,则有可能外部组件位于浏览器缓存中,并使用了外部文件。“动态内联”

由于每个用户开始的时候都没有cookie,因此必须有一种途径来引导这一过程。这可以通过使用前一个例子中的加载后下载技术来完成,当用户第一次访问页面时,服务器发现没有cookie,于是生成一个内联了组件的页面。然后服务器添加JavaScript来在页面加载后动态下载外部文件(并设置cookie)。下一次访问页面时,服务器看到了cookie,就会生成一个使用外部文件的页面

这种方式的美好之处是在于它的宽容。即便cookie的状态和缓存的状态不匹配,页面也能够工作,只是没有本应该的那么优化而已。基于会话的cookie技术在内联时会发生错误,即便组件已经被放到浏览器缓存中了--如果用户重新打开浏览器,基于会话的cookie会消失,但组件依然存在于缓存中。将cookie从基于会话的改为短期的(数小时或数天)可以解决这个问题。但当它们并不在浏览器缓存中时,使用外部文件又会出错。不管出现哪种情况,页面都能够工作,并且从全体用户的角度来看,它能够在内联和外部文件之间做出智能的选择,从而改善响应时间

规则9---减少DNS查找

Internet是通过IP地址来查找服务器的。由于IP地址很难记忆,通常使用包含主机名的URL来代替它,但当浏览器发送其请求时,IP地址仍然时必须的这就是Domain Name System(DNS)所处的角色。DNS将主机名映射到IP地址上,就像电话本将人名映射到他们的电话号码一样。当你在浏览器中键入www.yahoo.com时,连接到浏览器的DNS解析器会返回服务器的IP地址。

这个解释强调了DNS-URL和实际宿主它们的服务器之间的一个间接层---的另外一项优点。如果一个服务器被另外一个具有不同IP地址的服务器替代了,DNS允许用户使用同样的主机名来连接到新的服务器。或者,比如在www.yahoo.com的例子中,可以将多个IP地址关联到一个主机名,为网站提供高冗余度

然而,DNS也是开销,通常浏览器查找一个给定的主机名的IP地址需要花费20~120毫秒。在DNS查找完成之前,浏览器不能从主机名那里下载到任何东西。响应时间依赖于DNS解析器(通常由你的ISP提供)、它所承受的请求压力,你与它之间的距离和你宽带速度。在从浏览器的角度回顾完DNS的工作之后,我将介绍如何减少页面花在DNS查找上的时间

DNS缓存和TTL

DNS查找可以被缓存起来以提高性能。这种缓存可以发生在由你的ISP或局域网中的一台特殊的缓存服务器上,但我们这里要探索的是发生在独立计算机上的DNS缓存。在用户请求了一个主机名之后,DNS信息会留在操作系统的DNS缓存中(Microsoft Window上的“DNS Client服务”),之后对于该主机名的请求将无需进行过多的DNS查找,至少短时间内不需要

很多浏览器拥有其自己的缓存,和操作系统的缓存相分离。只要浏览器在其缓存中保留了DNS记录,它就不会麻烦操作系统来请求这个记录。只有当浏览器缓存丢弃了记录时,它才会向操作系统访问地址---然后操作系统或者通过其缓存来响应这个请求,或者将请求发送给一台远程服务器,这时就会发生潜在的速度降低

使事情更复杂的是,设计者知道IP地址会变化以及缓存会消耗内存。因此,应该周期性的清楚缓存中的DNS记录,并通过大量不同的配置设置检测清楚的频率有多高

影响DNS缓存的因素

首先,服务器可以表明记录可以被缓存多久。查找返回的DNS记录包含了一个存活时间(Time-to-live,TTL)值。该值告诉客户端可以对该记录缓存多久。

尽管操作系统缓存会考虑TTL值,但浏览器通常忽略该值,并设置它自己的时间限制。此外,序言B中讨论的HTTP协议中的Keep-Alive特性可以同时覆盖TTL和浏览器的时间限制。换句话说,只要浏览器和Web服务器愉快的通信着,并保持TCP连接打开的状态,就没有理由进行DNS查找

浏览器对缓存的DNS记录的数量也有限制,而不管缓存记录的时间。如果用户在短时间内访问了很多具有不同域名的网站,较早的DNS记录将被丢弃,必须重新查找该域名。不过,要记得即便浏览器丢弃了DNS记录,操作系统可能依然保留着该记录,这能扭转一下局面,因为无需通过网络发送查询,从而避免了明显的延迟

每个网站的TTL的值是不同的

客户端收到的DNS记录的平均TTL值只有最大TTL值的一般。这是因为DNS解析器自身也拥有与DNS记录相关的TTL。当浏览器进行DNS查找时,DNS解析器返回的时间是其记录的TTL的剩余时间。如果最大TTL是5分钟,DNS解析器返回的TTL范围可能是1~300秒,平均150秒。对于给定的主机名,每次执行DNS查找时接收到的TTL值都会变化

浏览器的视角

正如之前在“影响DNS缓存的因素”一节中讨论的那样。大量独立的变量决定了一个特定的浏览器在请求一个主机名时是否会进行远程DNS请求。尽管有DNS规范,但DNS缓存如何工作上,它给客户端留了较大的灵活性。我将着重介绍Microsoft Windows 上的Internet Explorer和Firefox,因为它们是最流行的平台

Microsoft Windows上的DNS缓存由DNS Client服务进行管理。你可以使用ipconfig命令来查看和刷新DNS Client服务

ipconfig /displaydns

ipconfig /flushdns

重新启动也可以清空DNS Client服务缓存。除了DNS Client服务之外,Internet Explorer和Firefox浏览器都有其自身的DNS缓存。重新启动浏览器会清空浏览器缓存,但不会清空DNS Client服务缓存

减少DNS查找

当客户端的DNS缓存为空(浏览器和操作系统都是)时,DNS查找的数量与Web页主机名的数量相等。这包括页面URL、图片、脚本样式、Flash对象等的主机名。减少唯一主机名的数量就可以减少DNS查找的数量。这方面的优秀示例是Google,它的页面只需要一次DNS查找

减少唯一主机名的数量会潜在的减少页面中并行下载的数量.避免DNS查找降低了响应时间,但减少并行下载可能会增加响应时间。正如第6章中“并行下载”一节中介绍的那样,一定数量的并行下载是好的,尽管增加了主机名的数量。对于Google.com来说,它的页面中只有两个组件。由于每个主机名可以并行下载两个组件,使用一个主机名既减少了DNS查找的数量,又最大优化了并行下载

今天很多的页面都有大量的组件---不会像Google那样。我的建议是将这些组件分别放到至少2个,但不要超过4个主机名下。这是在减少DNS查找和允许高度并行下载之间作出的很好的权衡

绪言B中介绍的使用Keep-Alive所带来的好处在于,它可以通过重用现有的连接,从而通过避免TCP/IP开销来减少响应时间。正如这里所介绍的,确保服务器支持Keep-Alive还能减少DNS查找,尤其是对Firefox用户来说

规则10---精简JavaScript

JavaScript作为一门解释型语言,是构建Web页面的首选。当以快速原型为基准开发用户界面时,解释语言要优于其他语言。由于没有了编译步骤,在最终部署前优化JavaScript的责任就落在了前端工程师身上。这个问题的一个方面---压缩---已经在第4章中讨论过了,这一章我将介绍在JavaScript部署过程中应该集成的另一个步骤---精简

精简

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

混淆

混淆(Obfuscation)是可以应用在源代码上的另外一种优化方式。和精简一样,它也会移除注释和空白,同时它还会改写代码。作为改写的一部分,函数和变量名将被转换为更短的字符串,这时的代码更加精炼,也更难阅读。通常这样做是为了增加对代码进行反向工程的难度,但这对提高性能也有帮助,因为这比精简更能减小代码的大小

如果你的目的不是抵制方向工程,就会遇到是使用精简还是使用混淆的问题。精简是一个安全并且箱单相当简单的过程。而混淆则更为复杂一些。混淆JavaScript有三个主要的缺点

缺陷

由于混淆更加复杂,混淆过程本身很有可能引入错误

维护

由于混淆会改变JavaScript符号,因此需要对任何不能改变的符号(例如API函数)进行标记,防止混淆器修改它们

调试

经过混淆的代码很难阅读。这使得在产品环境中调试问题更加困难

我从未见过精简会带来问题,但我见过由于混淆引起的缺陷。要是像Yahoo!这样维护庞大数量的JavaScript,我的建议是使用精简而不是混淆。最终的决定需要考虑混淆能够带来的额外的代码大小减少量。在下一节中,我们将要进行一些实际的精简和混淆

经过gzip压缩后的代码,精简和混淆之间的差距将会减小

锦上添花

还有很多其他途径能够减少你的JavaScript中浪费的时间

内联脚本

之前的讨论一直在关注外部的JavaScript文件。内联的JavaScript块也应该精简,尽管对于现今的网站这一实践的效果并不很明显

压缩和精简

规则4强调了对内容进行压缩的重要性,并建议使用gzip来完成压缩,这通常可以使大小减少70%。gzip压缩比精简更能减小文件的大小---这就是它是规则4而这是规则10 。我曾经听到有人问过,如果已经启用了gzip压缩,是否还值得进行精简。

gzip压缩产生的影响最大,但精简能够进一步减小文件大小。随着JavaScript的使用量和大小不断增长,精简JavaScript代码能够得到更多的节省

精简CSS

精简CSS能够带来的节省通常要小于精简JavaScript,因为通常CSS中的注释和空白比JavaScript少。最大的潜在节省来自优化CSS--合并相同的类,移除不适用的类等。CSS的依赖顺序的本质(这也是将其称为叠层样式表的原因)决定了这将是一个复杂的问题。这个领域还需要进一步研究和工具开发。最佳的解决方案还是移除注释和空白,并进行一些直观的优化,比如使用缩写(用#606代替#660066)和移除不必要的字符串(用0代替0px)

规则11---避免重定向

重定向(Redirect)用于将用户从一个URL重新路由到另一个URL。重定向有很多种---301和302是最常用的两种。通常针对HTML文档进行重定向,但通常也可能用在请求页面的组件(图片、脚本等)时。实现重定向可能有很多不同的原因。包括网站重新设计、跟踪流量、记录广告点击和建立易于记忆的URL。我们将再本章中讨论所有这些方面

重定向的类型

当web服务器向浏览器返回一个重定向时,响应中就会拥有一个范围再3XX的状态码。这表示用户代理必须执行进一步操作才能完成请求。以下是几种3xx状态码

300 Multiple Choices(基于Content-Type)

301 Moved Permancently

302 Moved Temporarily(亦称Found)

303 See Other(对302的说明)

304 Not Modified

305 Use Proxy

306 (不再使用)

307 Temporary Redirect(对302的说明)

“304 Not Modified”并不真的是重定向---它用来响应GET请求,避免下载已经存在于浏览器缓存中的数据,这曾在虚言B中介绍过。状态码306已经被废弃

状态码301和302是使用的最多的。状态码303和307是在HTTP1.1规范中添加的,用来澄清对302的使用(滥用),但是几乎没人采用303和307,绝大多数网站仍然在沿用302。下面是301响应头的一个示例

HTTP 1.1 301 Moved Permanently

Location:http://stevesouders.com/newurl

Content-Type:text/html

浏览器会自动将用户带到Location字段所给出的URL。重定向所必须的所有信息都出现在这个头中了。响应体通常是空的。不管叫什么名字,301和302响应在实际中都不会被缓存,除非有附加的头---如Expires或Cache-Control等---要求它这么做

还有其他地方可以自动将用户重定向到其他URL。HTML文档的头中包含的meta refresh标签可以在其content属性所指定的秒数之后重定向用户---

<meta http-equiv="refresh" content="0;url=http://www.baidu.com"/>

JavaScript也可以用于执行重定向,将document.location设置为期望的URL即可。如果你必须进行重定向,最好的技术是使用标准的3xxHTTP状态码,这主要是为了确保后退按钮能够正确工作

重定向是如何损伤性能的

在第5章中,我谈到了加快下载样式表的重要性,否则页面的呈现会被阻塞。类似地,第6章解释了为什么外部脚本会阻塞页面呈现并阻止并行下载。重定向引起的延迟也很严重,因为它延迟了整个HTML文档的传输。在HTML文档到达之前,页面中不会呈现出任何东西,也没有任何组件会被下载。在用户和HTML文档之间插入重定向延迟了页面中的所有东西

重定向通常伴随着对HTML文档的请求一起使用,但偶尔也会看到将它们用于页面中的组件

重定向之外的其他选择

重定向是解决很多问题的简单方式,但最好使用其他不会减慢页面的加载速度的解决方案。

缺少结尾的斜线

有一种重定向最为浪费,发生的也很频繁,但Web开发人员通常没有意识到它。它发生在URL的结尾必须出现斜线(/)而没有出现时。

当缺少结尾的斜线时发送重定向有着很充分的理由---它允许自动索引,自动跳转到默认的index.html上,并且能够获得与当前目录相关的URL。然而,很多流行的Web页面并不依赖自动索引,而是依赖特定的URL和处理器。另外,URL通常也与根目录相关而不是和当前目录相关

注意当主机名后缺少结尾斜线时不会发生重定向。例如http://www.baidu.com不会产生重定向。然而,你在浏览器中看到的最终的URL是包含结尾斜线的----http://www.baidu.com/。导致自动产生结尾斜线的原因是,浏览器是在进行GET请求时必须指定一些路径。如果没有路径,例如http://www.baidu.com,它就会简单地使用文档根(/)----

GET / HTTP 1.1

当缺少结尾斜线时发送重定向是很多Web服务器的默认行为,包含Apache,Alias指令是一种简单的方法。另一种选择是使用mod_rewrite模块,但Alias更为简单

如果你的网站包含目录并使用了自动索引,用户就必须忍受一个到达预期页面的重定向。检查以下你的Web日志就能看到发出了多少301状态码,这能帮助你认识到多么值得去解决结尾斜线的问题

连接网站

想象一下网站后端被重写的情形。这经常发生,新的实现中的URL很可能会有所不同。将用户从旧的URL转移到新的URL的最简单的方式就是重定向。重定向是使用定义良好的API---URL来整合两个代码基础的一种方式

将旧网站连接到新网站只是重定向这种常见应用中的表现形式之一。其他形式还包括将一个网站的不同部分连接起来,以及基于一些条件(浏览器类型,用户账户类型等)来引导用户。使用重定向来连接两个网站很简单而且只需要很少的额外代码。

Google Toolbar 页面加载时所需的多个重定向就是出于这个目的---连接网站。其后端网站有很多部分。在后端组件的新版本发布后,可以通过简单地更新重定向来将它们连接到主站点上

虽然重定向降低了开发地复杂性,但是这也损害可用户体验,正如前面“重定向如何损伤性能的”一节所介绍的那样。整合两个后端还有其他的选择,但比起简单的重定向需要更多的开发工作,不过这样非但不会损害用户体验,还能使之改善

ALias、mod_rewrite和DirectorySlash(前面在“缺少结尾的斜线”一节中介绍过)要求除URL之外还要提交一个接口(处理器或文件名),但易于实现

如果两个后端位于同一台服务器上,则它们的代码很可能自己就能够连接。例如,旧的处理器代码可以通过编程调用新的代码处理器

如果域名换了,可以使用一个CNAME(一条DNS记录,用于创建从一个域名指向另一个域名的别名)让两个主机名指向相同的服务器。如果能够做到这一点,这里提到的技术(Alias、mod_rewrite、DirectorySlash和直接连接代码)就是可行的

跟踪内部流量

跟踪出站流量

美化URL

规则14---使Ajax可缓存

Web 2.0、DHTML和Ajax

Web 2.0

O'Reilly Media在2004年第一次举办Web2.0大会时首次使用了我术语“Web 2.0”.该术语并不表示下一代Internet将会使用用于社会和商业方面的众多技术。它仅暗指了Internet社区中丰富的维基、博客和博客所带来的网站。Web2.0的关键概念包括类似应用程序的用户界面和来自多个Web Service的聚合信息。Web页面变得越来越像一个具有良好定义的输入、输出的应用程序。DHTML和Ajax是实现这些概念的技术

DHTML

动态HTML允许在页面加载完毕之后,HTML页面的表现能够变化。这使用JavaScript和CSS与浏览器的文档对象模型(Document Object Model,DOM)进行交互来实现。例如当用户将鼠标悬浮在链接上面时,页面表现会发生变化,如树状控件可以展开和折叠,页面中的层叠菜单也会变化。更复杂的DHTML页面可能会根据用户的意图重回整个页面,例如,从浏览Email的收件箱转到撰写邮件消息时,Ajax是DHTML中使用的一项技术,客户端可以获取和显示用户请求的新信息而无需重新加载页面

Ajax

术语“Ajax”由Jesse James Garrett于2005年提出(注1)。Ajax表示“异步的JavaScript和XML(Asynchronous JavaScript and XML)”(尽管今天除了XML由很多其他选择,最著名的是JSON)。Ajax不是一个单独的,需要许可证的记住,而是一组技术,包括JavaScript,CSS,DOM和异步数据获取。Ajax的目的是为了突破Web本质的开始-停止交互方式。向用户展示一个白屏然后重绘整个页面不是一种好的用户体验。而Ajax在UI和Web服务器之间插入了一层。整个Ajax层位于客户端,于Web服务器进行交互以获取请求的信息,并与表现层交互,仅更新那些必要的组件。他将Web体验从“浏览页面”转变为“与应用程序进行交互”

Ajax背后的技术比这个术语本身包含的要宽泛的多。IFrame---1996年首次由IE3引入---允许在页面中异步加载内容,至今仍有一些Ajax应用程序在使用这一技术。XMLHttpRequest---我认为是Ajax的心脏---1999年再IE中提出(名为XMLHTTP),2003年再Mozilla中出现。针对Ajax的W3C XMLHtppRequest规范提议于2006年4月发布

异步和即时

Ajax的一个明显的有点就是向用户提供了即时反馈,因为它异步地从后端Web服务器请求信息。在前面引用的Web文章中。Jesse James Garrett在提到“所有东西几乎都是即时的”

小心,使用Ajax并不保证用户就不会以便玩弄自己的手指以便等着“异步的JavaScript和XML”返回响应。在很多应用程序中,用户是否需要等待取决于如何使用使用Ajax。前端工程师有一次要担当起明确和遵守最佳实践的责任,以确保用户的体验能够很快

用户是否需要等待的关键因素在于Ajax请求是被动还是主动的。被动请求(Passive Request)是为了将来使用而预先发起的。例如,在一个基于Web的Email客户端中,可能会使用被动请求在用户真正需要下载用户的地址簿。经过被动加载,当用户需要为一个Email消息添加地址时,客户端能够确保地址簿已经存在于缓存中。主动请求(Active Request)是基于用户当前的操作而发起的。例如查找所有与用户的搜索条件相匹配的Email消息

后面的例子展示了,即便主动的Ajax请求时异步的,用户Kennedy仍然需要等待响应。不过我们真的还是应该感谢Ajax,用户不必然收整个页面的重新加载了,而且当用户等待时,UI仍然是可响应的。不过,用户在进行进一步操作之前仍可能需要坐在那里,等待搜索结果显示出来。记住“异步”并没有按时“即时”,这一点很重要。我非常认同Hesse James Garrett最后给出的FAQ

问:Ajax应用程序总是能够比传统Web应用程序提供更好的体验吗?

答:不一定。Ajax给交互设计师带来了更多的灵活性。然而,我们的能力越强大,在使用的时候越应该小心。我们必须小心的使用Ajax,确保它是优化了应用程序的用户体验,而不是恶化了

我还认同他的另一个评论---

会好的。

让我们开心的去探索如果使用Ajax优化用户体验,避免可能恶化用户体验的常见缺陷吧

优化Ajax请求

前面一节明确了当发起主动Ajax请求时,用户可能仍需等待。要改善性能,优化这些请求很重要。优化主动Ajax请求的技术同样适用于被动Ajax请求,但由于主动请求对用户体验的影响更大,所以我们从这里开始

要找出Web应用程序中所有的主动Ajax请求,请启动你喜欢的抓包工具。在Web应用程序加载完毕后,开始使用它,同时观察抓包工具中显示出来的Ajax请求就是必须进行优化以实现更佳性能的

改善这些主动Ajax请求的最重要的方式就是使响应缓存,如第3章中的讨论过的其他13个规则中有一些也适用于Ajax请求

规则4---压缩组件

规则9---减少DNS查找

规则10---精简JavaScript

规则11---避免重定向

规则13---ETag---用还是不用

然而,规则3是最重要的。我建议了一个新的规则,仅仅是在一个新的环境中简单的重申前面的规则,这有些不公平。但我发现,由于Ajax是如此崭新和不一般,这些性能改进必须明确地单独提出来

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值