zdlive是我大四实习的一家公司,当时在那边负责前端模块,写了蛮多的笔记,现在整理出来~=)
相比起后台的优化,前台的优化一般都会被忽视,但其实在用户发起一个页面请求,到页面最终展现给用户,前端所占的比重是非常大的。看了《高性能网站建设指南》,还有《构建高性能Web站点》的部分章节,针对zdlive的页面,可以做如下优化尝试。
优化的指导方针共有13条,结合zdlive讲。
一般我们的网页会有如下三种,图片、css文件、js文件和html文件。当用户在输入输入网址的时候,浏览器会向服务端发起一个http请求。当这个html文件被下载到客户端之后,其他的组件才开始下载,我们以zdlive.com为例。
可以看到,所有的组件下载,都是等到第一个文件载入完毕后再发起的,而这里的每一项都是代表着一个http请求。
对于一个页面来说,多个http头并没有什么关系,但对于响应速度和稳定性要求高的,比如zdlive的首页,尤其是gprs这样坑爹的网络下,多个http头,意味着速度慢,请求失败的几率会很大,页面显示不完整等等问题。
那我们要做的就是减少http请求了。
方法有如下几种:
对于我们的首页来说,这种做法还需要实际测试下。
坏处是Base64位压缩以后,图片的大小会变大。但结合服务端的gzip压缩,减少客户端的8个请求,还是可以尝试下的。
对于一个文件来说,通常会有多个脚本文件和样式文件,分开是模块管理的需要,但有时候也可以结合在一起,合并下载。但这个单个文件大小,和多个http请求头之间的怎么去权衡,我还不是很清楚。 ## 使用内容分发网络 内容分发网络的意思是说,假设我们公司现在有多台服务器,一些专门用来跑应用支持,一些专门用来给用户下载文件的,比如img文件、mp3文件等等。而这里,内容分发网络就是指这些用来下载的服务器了。只是,他们现在都在北京这个地方,如果这些被用户用来下载文件的服务器能部署在全国各地,那对于用户来说,更近的服务器,就意味着更短的相应时间和更快的下载速度。
拿douban的豆瓣FM来说,当我发起访问的时候,实际上我听的歌曲并不是在北京那边的服务器给过来的,倒是从广州这边的服务器下载的。
具体测试,可以用下抓包工具查看ip地址信息,海富推荐用fiddler2。
一般浏览器都会缓存页面请求的组件,比如图片、js文件和css文件,这样能够减少客户端发起无谓的请求,加快页面响应速度。
但问题来了,浏览器怎么知道这些组件已经过期了呢?原来,浏览器和服务器之间是有协议的,这个协议是通过访问Http头来达到合作的目的。一般,当客户端发起一个请求的时候(走http1.1的协议),http里面就会带有很多的信息,其中有3个http头Expires、last modified、Cache-Control就是两者用来沟通缓存信息的,而我们要做的就是如下三点:
下面,我们以一个交互过程说明下。假设当前用户的本地缓存里面没有我们网站的信息。
字段说明: - Accepttext/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8 告诉服务器自己接收什么类型的文件,比如html、xml等文件 - Accept-Encoding:gzip,deflate 告诉服务器,浏览器自身支持gzip、deflate的压缩格式,可以发送这两种压缩格式的压缩包过来。 - Accept-Language:zh-cn,zh;q=0.5 告诉服务器自己接受中文 - Connection:keep-alive 保持长久连接 - Cookie:… 一般如果这个网站有cookie的话,浏览器也会并着请求一起发送过去。 - Host:zdlive.com:8080 就是我们服务器的主机地址了 - User-Agent:Mozilla/5.0 …对于这个字段,表明自己的浏览器类型,每个字段都有特殊的含义,可以用来做机型适配等工作。http://www.useragentstring.com/index.php
当服务器收到客户端的请求后,返回相关的相应头。
对应的字段简单说下: - Connection:keep-Alive 保持传输http数据的tcp连接,当客户端再次向服务器请求的时候,沿用这个连接。 - Content-Type:text/html 告诉服务器返回的文件类型是什么,这里我们看到是html类型。 - Date:wed,… 这里是标识请求访问的日期。 - Keep-Alive: timeout=5,max=100 5秒后无连接的话就会关闭连接,最大连接次数100次 - Server:Apache… 告诉客户端,服务器的类型,运行脚本的类型,这里是Apache,还有php - Transfer-Encoding:chunked表示Content Body将用Chunked编码传输内容。
这样一个来回后,用户请求的页面文档已经下载到本地了,然后其他组件才开始下载。
大家可以看到,在这里,初次访问时,浏览器都是发起GET请求,服务器对请求的资源进行相应,返回200标识资源请求成功。
好了,那当用户第2次访问我们网页的时候,会发生什么事情呢?慢着,这里面要分情况了。
假设我们在firefox里面,而且我们的服务器,比如Apache配置了静态文件压缩、对Expires的支持等。
firefox浏览器刷新的方式共有3种,分别是Ctrl+F5,F5,“转到”三种,而这3种刷新方式,都对应着不同的缓存协议。
看到200状态码和大小了吗,这里浏览器还是从服务器那里重新下载了所有的资源,完全忽视了本地的缓存。
F5的方式,使用缓存协议,如果存在last modified的话,向服务器发起if since modified的get请求,对于动态内容,服务器需要做对应的请求响应。而静态内容,可以在Apache的配置文件里,写上对应的文件类型即可。
“转到”方式,即一般在url输入栏里输入地址,回车进行访问,这个时候,浏览器会对请求文件使用更加完善的缓存协议,包括检查f5的方式之外,还会检测文件的expires属性,如果当前时间仍在expires允许的范围内的话,就不会发起get请求,直接使用缓存的内容。
上面的2、3的请求和响应动作,其实都是基于服务器的主动缓存工作,这块内容有很多,书中提到的几点是针对静态内容的缓存:
通常形式如下:
Cache-Control:max-age=315370000
而在Apache中,可以这样配置Expires:
1 2 3 4 5 6 7 8 |
|
而对于动态内容,其实我们也可以做类似这样的缓存操作,比如在php中,我们可以让内容在1个小时内保持有效,代码如下:
1 2 3 4 5 6 7 8 |
|
使用上面的缓存技术一个坏处就是如果我们想更新的时候,用户的缓存可能还没过期。
对于更新静态缓存,作者提出了修改文档组件名字的方式,具体还是要结合我们自己的实践。
目前采用两种方式,一种是对文件本身进行“瘦身”,另外一种就是在服务器端开启mod_gzip模块,对特定类型的文件进行压缩,比如我们现在要用到的css、js和html文件。
对于IE6-8,如果我们把样式表放在页面底部的话,浏览器会一直等待,直到底部的样式文件都被下载下来了,才会显示页面出来。这段时间,用户只能看着白屏,然后看到页面瞬间跑出来。这个原因是因为,ie浏览器的渲染引擎认为,如果你的样式文件放在底部,我一开始就渲染会浪费资源,不如等到你的样式文件都下到本地了,我再渲染也不迟。
而另外一个原因是因为,其他非ie的浏览器,在底部的样式文件下载下来之前,会进行页面的渲染工作,假设这样一种情况,我们的全局样式文件100s之后才下载下来,这个时候,其实页面早就显示了,只是很丑陋,没有任何样式,这个是页面逐步呈现的结果。
针对这两种情况,作者才这样建议把样式放在顶部,既避免丑陋的页面出现,又能利用页面默认逐步呈现出来的效果,给到用户一种页面在加载的感觉,而这样的视觉提示是非常有用的,可以缓解用户等待的焦虑感。
关于页面从请求成功,到渲染完毕,这个过程有点复杂,不过可以参考如下几篇文章和书籍:
将脚本放在代码底部的原因是因为,js代码的运行,会阻塞后续资源的请求。而这个也是浏览器干的好事。现代浏览器认为,你的js代码里面可能有些代码会重写页面的DOM,比如$(“download_img”).remove();,那么如果浏览器在你运行js脚本的同时去请求资源的话,有可能你就改掉了那个img文件了,这样浏览器就不会去请求资源,而是等到js执行完毕之后再去进行这个操作。
对于这种情况,玉伯总结过一些规律,如下:
JS 有可能会修改 DOM. 典型的,可能会有 document.write. 这意味着,在当前 JS 加载和执行完成前,后续所有资源的下载有可能是没必要的。这是 JS 阻塞后续资源下载的根本原因。
JS 的执行有可能依赖最新样式。比如,可能会有 var width = $(‘#id’).width(). 这意味着,JS 代码在执行前,浏览器必须保证在此 JS 之前的所有 css(无论外链还是内嵌)都已下载和解析完成。这是 CSS 阻塞后续 JS 执行的根本原因。
现代浏览器很聪明,会进行 prefetch 优化。性能是如此重要,现代浏览器在竞争中,在 UI update 线程之外,还会开启另一个线程,对后续 JS 和 CSS 提前下载(注意,仅提前下载,并不执行)。有了 prefetch 优化,这意味着,在不存在任何阻塞的情况下,理论上 JS 和 CSS 的下载时机都非常优先,和位置无关。
现代的浏览器,比如firefox,其实会对页面的组件提前进行下载prefetch,而不管这个文件是在什么一个位置。当然了,只是firefox这样做了,其他浏览器还有待测试,只是通过这点可以知道,书是不可靠的=.=~,动手测试才是硬道理!
Css表达式是ie系列特有的,它会调用js里面的函数,从而达到动态样式的效果,不过有很多bug,所以作者说避免使用css表达式。
一般我们都会把html文档里面的,js和css文件用标签加载进来,这样的好处是,代码容易管理,但坏处是,如果这些文件非常多的话,一个页面就会发起多个http请求,对于网络不稳定的GPRS,多个http请求意味着有可能丢失,或者请求失败这些情况。
如何平衡好这些分离的问题,可以从下面三个标准去看。
每个用户会话的页面查看次数。如果用户每次查看的页面数量多的话,可以考虑把文件js和css文件抽离出来,让浏览器进行缓存。
组件重用的程度。每个页面的js和css文件都有重叠的部分,对于重用率高的代码抽离出来,可以让所有页面共享浏览器缓存。但这种行为要视乎用户访问我们页面的行为,如果每次会话,跨页访问的频率高的话,就可以这样做。当然了,还有其他的一些情况。
用户访问我们页面的空缓存和完整缓存的比例。Yahoo的开发团队做个一个测试,就是用户访问Yahoo首页的时候,用户浏览器的cache情况,结果如下图:
这个图里面,单看黄色线,发现大部分用户访问的时候,cache都是空的。好了,如果是针对这种情况,那加快首页速度的方法就是使用内联的样式表和脚本了。当然,为了减少整体页面大小,可以对内联的脚本和css文件再进行压缩。
DNS查询的意思是,当我们输入zdworks.com的时候,这个域名需要被解析成ip地址,请求才能到达我们的服务器,而这个去查询域名ip地址的时间,是需要时间的。
对于这种情况,浏览器和pc都有缓存dns记录的习惯,而我们可以做的就是增加我们域名在用户的两个缓存记录里面的ttl值,ttl即“生存时间(Time To Live)”。 ## 精简js 就是我们之前说到的用JSMIN和YUI的压缩工具进行压缩了。
重定向会延缓用户打开页面的时间,应该尽量减少。书中提到一点很重要,缺少结尾的斜线,这样的域名会导致重定向,而这个不是我们预期的。
比如,m.clock.zdworks.com/index 会引起服务器的重定向,重新定位到m.clock.zdworks.com/index/,然后根据我们的配置,掉落到index.html.
作者提到在大型项目这样的情况可能会比较多,重复脚本对性能的影响也是有的。处理的方式很多,作者提到的方式是在后台,采用函数去判定加载到当前页面的js文件是否已经存在。
Etag是实体标签(Entity Tag,ETag)是服务器和浏览器用于确认缓存组件的有效性的一种机制。
在使用和配置上面还是有许多问题,详细还是要看下配置文档。
对Ajax请求的资源,返回Expires等http头,让浏览器缓存数组。在zdlive的首页中,就是通过在页面加载完毕后,发起异步请求,把图片先加载下来,最后再插入到页面中的。而在这个插入的过程中,是给了Img.src=””赋值,而这个动作非常关键,因为一旦执行,页面就会发起http请求,这个时候,因为之前ajax请求的图片资源带有expires头,浏览器就会去判断当前时间是否超过了图片的expires头规定的时间,如果没超过的话,就利用缓存的数据,从达到异步请求的效果。
这方面的书籍也很多,推荐《Ajax in action》、《Head.First.Ajax》.
浏览器为了保护用户的正常使用,对单个页面的并行下载数量是有限制的。比如在桌面版的firefox,并行下载数量就是8个,当然这个我们可以通过about:config那边去进行设置。
对于这种限制,我们可以通过把组件分离,分发到不同的域名服务器下面,从而绕过这个限制。当然了,这个对于静态文件还是比较好做的,但对于动态生成的文件就不好弄了。
HTTP1.1中建议主机最大并行下载4个组件,而HTTP1.0协议的话,好像不存在这样的情况。那我们要做的就是,对http1.1的请求进行降级,绕过这个协议的限制。