三。 减少用户行为造成的无用请求 : 支持渐进式页面生成 。
网页加载时间过长的危害是多方面的。糟糕的用户体验不仅不能丢掉已有用户,用户还有可能放弃正在进行中的交易;同时网页性能低下也影响企业专业形象,进而影响用户对该网站交易安全性,该网站财务的稳健性的印象; 最后,网页显示迟缓会反过来影响用户行为,用户可能反复点击重试,产生额外的信息请求,进而增加了不必要的服务器负载。UX中有一个2-5-10原则:简单说,就是当用户能够在2秒以内得到响应时,会感觉系统的响应很快;当用户在2-5秒之间得到响应时,会感觉系统的响应速度还可以;当用户在5-10秒以内得到响应时,会感觉系统的响应速度很慢,但是还可以接受;而当用户在超过10秒后仍然无法得到响应时,会感觉系统糟透了,或者认为系统已经失去响应,而选择离开这个Web站点,或者发起第二次请求 。
用户的反复点击重试如何增加系统负载呢? Apache MaxClients 设定了处理
并发HTTP请求的上限,如果MaxClients= 256,服务器端生成一个页面需要200 ms,而用户平均点击产生页面请求的频率是每8秒一次,一个Apache可以处理 256 × 8 / 0.2 = 10240 用户;而当用户平均点击产生页面请求的频率是每1秒一次,一个Apache只能处理 256 × 1 / 0.2 = 1280 用户。
Yahoo的Steve Sounders的重要著作<高性能网站的14个规则>中总结了缩短网页加载时间的14条规则。其中最重要的法则是减少HTTP请求, (其他还有符合浏览器工作原理的代码实现, 压缩减少相应数据大小)。
(一)减少HTTP请求的规则
首先介绍一下HTTP并发连接限制的一些背景。
HTTP1.1 规范建议: “单个用户和任何服务器/代理之间不要维持超过2 个连接“。 IE6严格遵守了此规范,但是各种最新的浏览器都没有遵守此规范。比如IE8在宽带入网情况下可以同时打开6个连接。
HTTP1.1规范中限制的是主机名,而不是IP地址值。因此前台工程师可以简单地使用CNAME(DNS别名),把组件分布到多个主机名上。这样并行下载数量可以任意增加。
但是打开的连接数对性能的影响是不是越多越好呢? HTTP并发连接也遵守边际报酬递减法则。更多的HTTP连接可以减少HTTP请求的排队等待时间;另一方面太多的连接会阻塞网络通讯, 并且服务器可同时打开的连接数也是有上限的。
结论是,一定程度的并发连接的性能好于只有一个HTTP连接;但是并发连接的数量是有限制的。 这种情况下需要尽可能的减少HTTP请求。
0. 使用缓存技术
上面已经介绍了Cache-control, Expires 和 ETag 及其相关技术, 此处不再重复。
1. 减少HTML文档组件的数量
Steve Sounders提出了缩短网页加载时间的性能黄金法则:终端用户响应时间的10-20%是用于HTML文档的下载请求;余下的80%-90%的时间是用来完成所有组件(图片,脚本,样式表,Flash等)的下载请求。因此,提高响应时间一个简单的方法是减少组件的数量,从而降低HTTP请求数。
-
图片的合并
常见的图片合并技术包括CSS图像拼合技术 (CSS Sprites),内联图片(Inline Images)和图像映射(Image Map)。图像映射允许一个单一的图像包含多个URL;内联图片使用URL Schema 数据请求, 可以不通过额外的HTTP下载网页中的图像。近年来CSS图像拼合技术被顶级网站甚至脚本库(比如JQuery库)广泛使用。
CSS图像拼合技术 和影像地图一样通过合并图像而减少服务器请求,而且更为灵活。 另外一个比较反直觉的地方是减少了整体图像的大小。 原因是合成图像减少了单独的图像的参数(颜色表,格式信息等)的开销。最后CSS图像拼合技术 使得CSS调用起来更为规则,很容易支持不同页面风格的切换。 总之,CSS图像拼合技术是一个优雅的解决方案, 带来整洁干净的标记,较少的图像和更快的响应时间。
-
合并脚本(Javascript)和样式表(CSS)
这里要考虑的是两方面的权衡: A) 按照软件工程学原理和模块化要求把代码分解成许多小文件 B)每个文件都引入一个附加的HTTP请求。
很多公司在开发过程中把脚本和样式文件按照需要模块化,在部署时把文件合并成一个单独文件,然后使用GZIP 和 Minifier 压缩后传给客户端。
但是这个策略没有充分考虑到缓存的效率。如果网站上不同页面使用部分相同的脚本/样式表代码, 这部分代码独立出来有利于组件重用。因此外部JavaScript 和CSS文件的细分策略影响组件重用的水平。
对于手机上网面临的网络延迟比较严重的情况,减少HTTP请求是至关重要的。JS和CSS内联将减少HTTP请求。 但是内联不仅使得 HTML文档内容过于臃肿,而且没法有效利用缓存。bing.com 使用了 HTML5 本地储存 技术解决了这个问题。当浏览器第一次访问某网站时,JS和CSS是内联的。 JS做了两件额外的工作:把内联的JS和CSS存放在本地存储 ; 同时标记cookie,表明JS和CSS已经存储在本地存储。当浏览器第二次访问该网站,服务器从cookie知道浏览器已经存储量相关的JS和CSS。因此,返回的HTML文档含有内联JS代码来读取本地存储的JS和CSS,并插入到DOM中。
-
减少DNS查找
浏览器通常需要20-120毫秒从给定的主机名查找到IP地址; DNS查找是阻断性的,查找完成之前,浏览器无法下载任何东西。 DNS需要查找网页中出现的所有不同主机名,包括图像,脚本,CSS,Flash等的URL中出现的主机名。
虽然大多数浏览器和操作系统缓存DNS查询结果, 但是由于主机对应的IP地址有可能改变,并且缓存有容量限制,DNS记录会被定期从缓存中清除。因此早期的DNS记录会被丢弃 。
HTTP协议 的Keep-Alive,可以同时忽略操作系统缓存TTL(生存时间)和浏览器的时间限制,进而减少DNS查找。
为了减少DNS查找,Steve Sounders 建议使用两个,最多不超过4个主机名。
-
避免重定向
Web服务器返回浏览器的3xx范围内的状态码属于重定向状态码。重定向所有必要的信息在返回的标头中设定,响应的主体通常是空的。浏览器然后会自动加载(Location )字段中指定的URL。重定向显然造成整个HTML文档的延误, 那为什么实践中还会使用重定向呢? 重定向可以把用户从旧过渡到新的网址,简单的整合两个代码库,跟踪网络流量,产生更简洁的网址。 所有使用重定向的动机都可以找到替代方案。
(二)。符合浏览器工作原理的代码实现
研究表明,Amazon是用户调查中反映加载迅速的顶级网站,其实其页面内容全部加载需要很多时间。 用户体验和实际网页加载速度的偏差在于渐进生成的技巧。
我们要在浏览器中尽快的显示的任何可显示的内容,逐步加载一个页面; 而不
是等到页面所有内容都下载后才生成页面。 渐进生成的策略为用户及时提供了加载进度的视觉反馈,尽可能早的为用户提供可有价值信息,减轻了用户感觉时间被浪费的等待的痛苦。
Steve Sounders讨论了在Yahoo公司的一个项目调试中遇到一个实例,HTML文档中错误的CSS/脚本位置阻碍了浏览器渐进生成页面的过程。
-
将CSS放在页面代码顶部
浏览器在下载所有的CSS前并不生成页面。如果在CSS没有被完全加载前就开始产生页面UI, CSS会重新改变页面UI而产生闪屏现象。
因此,CSS不能放在页面代码底部, 否则浏览器在等待页面底部的CSS文件加载 而出现“空白屏幕”现象。
-
将脚本放在页面代码底部
Steve Sounders在<高性能网站的14个规则>一书中引入了阻断性和非阻断性HTML 标签的概念。script标签阻断了页面的渐进式生成和页面资源的下载。老版本的浏览器甚至不支持script的并行下载, 以保证脚本以正确的顺序被解释执行; 新的浏览器支持script和CSS的并行下载,但是IE8依然阻断了图片和iframe的下载。
避免 script标签阻断页面的渐进式生成和资源下载的方法有几种。显然如果script像通常情况下放在页面代码顶部,在script加载结束前script后面整个页面的下载和生成都被阻止。因此需要将脚本放在页面代码底部。
-
控制脚本的下载和执行
如果把脚本移动到文档底部有困难,可以使用递延脚本。 defer属性表示该脚本不包含 document.write 指令,不直接影响页面的UI,提示浏览器可以继续渐进式生成网页。
HTML5 引入了脚本的async 属性。async 属性规定一旦脚本可用,则会异步执行。 async 属性仅适用于外部脚本(只有在使用 src 属性时)
<script type="text/javascript" async src="foo.js"></script>
越来越多的网站 使用脚本来控制脚本的异步下载。有很多Javascript Loader 的版本,比较常见的有 LABjs , HeadJS, ControlJS,RequireJS, Load.js, YepNope.js, $script.js, LazyLoad,curl.js,DeferJS ,jquery.defer.js ,jQl,YUI 3 Get, DominateJS,JSL, Bootstrap,StealJS,bdLoad 等。 选择的因素包括文件大小、可否定义依赖关系/执行顺序、可否并行加载,可否分离下载和执行等。
LABjs 是比较常用的Javascript Loader,定义维护脚本的依赖关系/执行顺序, 比如下载多个有相互依赖关系的脚本;而ControlJS提供了脚本下载和脚本执行的分类, 用于网页设计的渐进性加强模式,脚本下载后并不立刻执行。
下面是使用Lab.js的例子。
<script> //可以同时运行多条$LAB链,但是它们之间是相互独立,不存在次序关系 $LAB .script("core.js").wait() //core.js 加载执行之后再并行加main.js .script("main.js") //和plugin.js .script("plugin.js") .wait(function(){ //main.js, plugin.js加载后执行函数 plugin.init(); main.init(); main.doSomething(); }); </script> |
RequireJS 通用异步模块定义(Asynchronous Module Definition,AMD)同时获得了JS代码的模块化。
//定义模块ID,依赖关系数组,工厂函数。 //只有当 依赖关系数组中的所有的依赖性函数被加载和执行后,//工厂函数才被执行 define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {
//Define the module value by returning a value. return function () {}; });
|
再强调一下几种执行外部脚本的方法:
如果 使用async: 脚本相对于页面的其余部分异步地执行(当页面
继续进行解析时,脚本将被同时执行)
如果 只使用defer: 脚本将在页面完成解析时执行
如果既不使用 async 也不使用 defer:立即读取并执行脚本,浏览器暂停解析剩余页面,
动态生成脚本的DOM节点, 使用脚本来控制脚本的异步下载。
总结:
对前端工程师而言,合理的设计在满足用户体验(UX)的同时可以减少对服务器的负载。从数据信息量本身而言, 一则可以使用延迟加载 来减少完成事务所需要的动态数据内容, 二则可以使用 浏览器缓存来减少完成事务所需要的静态资源请求;三是压缩网页的图片、CSS、JavaScript等所关联的各种文件大小直接减少数据量。 从用户体验而言,支持渐进式页面生成,来减少用户非理性行为造成的无用请求 。
模式名称:延迟加载数据
描述:数据在真正需要使用时再请求加载。
动机/试图解决问题: 一次性加载过多数据造成页面生成缓慢,对用户的UI反应迟缓,同时增加了服务器负载。
原理: 从认知角度来看,人脑一次性可以接受的信息量有限。按照用户感兴趣程度以分页的方式提供数据,或者按照自然的认知路径来分步提供数据, 可以大幅度减少不必要的数据加载。
使用: 数据量过大,引起页面加载迟缓时。
相关模式:分页(Pagination), 支持延迟加载的表格。
模式名称:使用浏览器缓存
描述: 使用浏览器缓存存储静态资源,减少后续访问的HTTP下载。
动机/试图解决问题: 。
原理: 由于访问行为的可重复性和网站网页布局及部分插件资源的不变性,在客户端使用缓存技术来存储静态资源可以大幅度减少从服务端读取的总信息量。
使用:
相关模式: Cache-control ,Expires , Last-Modified 和 ETags, HTML5的local storage。
模式名称:减少HTTP请求次数
描述: 减少网页加载时的HTTP请求次数,以加速页面生成速度。
动机/试图解决问题: 在页面生成过程中,HTTP请求占用时间最多。
原理: 性能黄金法则指出终端用户响应时间的10-20%是用于HTML文档的下载请求;余下的80%-90%的时间是用来完成所有组件的下载。 虽然使用CNAME很容易实现任意程度的 HTTP并行请求,实际上并行程度并不是越高越好。 减少页面组件数目是减少HTTP请求的直接办法。
使用:
相关模式:缓存,CSS图像拼合技术, 合并脚本,减少DNS查询,减少URL重定向。
模式名称:支持渐进式页面生成
描述: 避免阻断性HTML Tag影响渐进式页面生成。
动机/试图解决问题: HTML文档中错误的CSS/脚本位置阻碍了浏览器渐进生成页面的过程 。
原理: 浏览器在下载所有的CSS前并不生成页面,必须将CSS放在页面代码顶部。
script标签阻断了页面的渐进式生成和页面资源的下载, 因此需要放在页面代码底部, 或者使用defer|async属性改变script下载的行为; 也可以动态生成脚本的DOM节点, 比如LABjs等JS Loader。
使用:
相关模式: