前言

zdlive是我大四实习的一家公司,当时在那边负责前端模块,写了蛮多的笔记,现在整理出来~=)

相比起后台的优化,前台的优化一般都会被忽视,但其实在用户发起一个页面请求,到页面最终展现给用户,前端所占的比重是非常大的。看了《高性能网站建设指南》,还有《构建高性能Web站点》的部分章节,针对zdlive的页面,可以做如下优化尝试。

优化的指导方针共有13条,结合zdlive讲。

减少HTTP请求

一般我们的网页会有如下三种,图片、css文件、js文件和html文件。当用户在输入输入网址的时候,浏览器会向服务端发起一个http请求。当这个html文件被下载到客户端之后,其他的组件才开始下载,我们以zdlive.com为例。

firebug截图

可以看到,所有的组件下载,都是等到第一个文件载入完毕后再发起的,而这里的每一项都是代表着一个http请求。

对于一个页面来说,多个http头并没有什么关系,但对于响应速度和稳定性要求高的,比如zdlive的首页,尤其是gprs这样坑爹的网络下,多个http头,意味着速度慢,请求失败的几率会很大,页面显示不完整等等问题。

那我们要做的就是减少http请求了。

方法有如下几种:

  • 减少图片。目前比较常见的做法是把多张图片整合到一张图片里面,然后利用css的技术,比如background的定位来做到共享图片的目的,这种技术叫做CSS Sprites。 坏处有2,定位麻烦和单张图片的大小会很大,这样用户请求到完成的时间就会比单张小图片要久。

对于我们的首页来说,这种做法还需要实际测试下。

  • 内联图片。对图片进行base64位的编码,这样图片就可以和html文档一起下载到客户端了。

坏处是Base64位压缩以后,图片的大小会变大。但结合服务端的gzip压缩,减少客户端的8个请求,还是可以尝试下的。

  • 合并脚本和样式表。

对于一个文件来说,通常会有多个脚本文件和样式文件,分开是模块管理的需要,但有时候也可以结合在一起,合并下载。但这个单个文件大小,和多个http请求头之间的怎么去权衡,我还不是很清楚。 ## 使用内容分发网络 内容分发网络的意思是说,假设我们公司现在有多台服务器,一些专门用来跑应用支持,一些专门用来给用户下载文件的,比如img文件、mp3文件等等。而这里,内容分发网络就是指这些用来下载的服务器了。只是,他们现在都在北京这个地方,如果这些被用户用来下载文件的服务器能部署在全国各地,那对于用户来说,更近的服务器,就意味着更短的相应时间和更快的下载速度。

拿douban的豆瓣FM来说,当我发起访问的时候,实际上我听的歌曲并不是在北京那边的服务器给过来的,倒是从广州这边的服务器下载的。

具体测试,可以用下抓包工具查看ip地址信息,海富推荐用fiddler2。

添加Expires头

一般浏览器都会缓存页面请求的组件,比如图片、js文件和css文件,这样能够减少客户端发起无谓的请求,加快页面响应速度。

但问题来了,浏览器怎么知道这些组件已经过期了呢?原来,浏览器和服务器之间是有协议的,这个协议是通过访问Http头来达到合作的目的。一般,当客户端发起一个请求的时候(走http1.1的协议),http里面就会带有很多的信息,其中有3个http头Expires、last modified、Cache-Control就是两者用来沟通缓存信息的,而我们要做的就是如下三点:

  • 增加last modified头,减少服务器的传输成本。
  • 增加Expires头,让浏览器本地可以直接访问资源,减少客户端不必要的请求。
  • 增加CaChe-Control头,程序或apache的配置文件中设置(静态资源),使用max-age。

下面,我们以一个交互过程说明下。假设当前用户的本地缓存里面没有我们网站的信息。

客户端

字段说明:

  • 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种刷新方式,都对应着不同的缓存协议。

  • Ctrl+F5的方式,强制浏览器刷新,所有组件全部重新请求,忽略和服务端的缓存协议,即忽略expires头,last modified等。

看到200状态码和大小了吗,这里浏览器还是从服务器那里重新下载了所有的资源,完全忽视了本地的缓存。

  • F5的方式,使用缓存协议,如果存在last modified的话,向服务器发起if since modified的get请求,对于动态内容,服务器需要做对应的请求响应。而静态内容,可以在Apache的配置文件里,写上对应的文件类型即可。

  • “转到”方式,即一般在url输入栏里输入地址,回车进行访问,这个时候,浏览器会对请求文件使用更加完善的缓存协议,包括检查f5的方式之外,还会检测文件的expires属性,如果当前时间仍在expires允许的范围内的话,就不会发起get请求,直接使用缓存的内容。

上面的2、3的请求和响应动作,其实都是基于服务器的主动缓存工作,这块内容有很多,书中提到的几点是针对静态内容的缓存:

  • 增加last modified头,这样能让浏览器发起一个条件get请求,减少重复的下载。
  • 因为last modified头只是指定一个固定时间,而且存在客户端和服务器时钟同步的问题,针对这种情况,http1.1提出了max-age和mod_expires的概念, max-age会在组件被请求时间的基础上进行叠加。

通常形式如下:

Cache-Control:max-age=315370000

而在Apache中,可以这样配置Expires:

1
2
3
4
5
6
7
8
<ifModule mod_expires.c>
ExpiresActive on
ExpiresByType img/png "access plus 24 hour"
ExpiresByType text/css "now plus 2 day"
ExpiresByType text/x-javascript "now plus 2 day"
ExpiresByType text/html "now plus 1 day"
ExpiresDefault "now plus 1 day"
</ifModule>

而对于动态内容,其实我们也可以做类似这样的缓存操作,比如在php中,我们可以让内容在1个小时内保持有效,代码如下:

1
2
3
4
5
6
7
8
<?php
$modified_time=$_SERVER[‘HTTP_IF_MODIFIED_SINCE’];
If(strtotime($modified_time)+3600>time()){
Header(“HTTP/1.1 304”);
exit(1);
}
header(“Last-Modified:”.gmdate(“D,d M Y H:i:s).” GMT”);
?>

使用上面的缓存技术一个坏处就是如果我们想更新的时候,用户的缓存可能还没过期。

对于更新静态缓存,作者提出了修改文档组件名字的方式,具体还是要结合我们自己的实践。

压缩组件

目前采用两种方式,一种是对文件本身进行“瘦身”,另外一种就是在服务器端开启mod_gzip模块,对特定类型的文件进行压缩,比如我们现在要用到的css、js和html文件。

  • 针对js文件,目前是采用YUI的压缩工具,从压缩效率来看,比JSmin要高。
  • 针对css文件,也是采用YUI的压缩工具,比其他的压缩工具优秀的地方在于,它把所有的代码整理成一行,而其他的压缩工具都是多行,在大小上可以节省不少。 压缩工具地址:传送门
  • 服务器端的gzip压缩可以参考服务器的配置文档。

将样式表放在顶部

对于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表达式

Css表达式是ie系列特有的,它会调用js里面的函数,从而达到动态样式的效果,不过有很多bug,所以作者说避免使用css表达式。

使用外部js和css文件

一般我们都会把html文档里面的,js和css文件用标签加载进来,这样的好处是,代码容易管理,但坏处是,如果这些文件非常多的话,一个页面就会发起多个http请求,对于网络不稳定的GPRS,多个http请求意味着有可能丢失,或者请求失败这些情况。

如何平衡好这些分离的问题,可以从下面三个标准去看。

  • 每个用户会话的页面查看次数。如果用户每次查看的页面数量多的话,可以考虑把文件js和css文件抽离出来,让浏览器进行缓存。

  • 组件重用的程度。每个页面的js和css文件都有重叠的部分,对于重用率高的代码抽离出来,可以让所有页面共享浏览器缓存。但这种行为要视乎用户访问我们页面的行为,如果每次会话,跨页访问的频率高的话,就可以这样做。当然了,还有其他的一些情况。

  • 用户访问我们页面的空缓存和完整缓存的比例。Yahoo的开发团队做个一个测试,就是用户访问Yahoo首页的时候,用户浏览器的cache情况,结果如下图:

这个图里面,单看黄色线,发现大部分用户访问的时候,cache都是空的。好了,如果是针对这种情况,那加快首页速度的方法就是使用内联的样式表和脚本了。当然,为了减少整体页面大小,可以对内联的脚本和css文件再进行压缩。

减少DNS查找

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

Etag是实体标签(Entity Tag,ETag)是服务器和浏览器用于确认缓存组件的有效性的一种机制。

在使用和配置上面还是有许多问题,详细还是要看下配置文档。

使用Ajax缓存

对Ajax请求的资源,返回Expires等http头,让浏览器缓存数组。在zdlive的首页中,就是通过在页面加载完毕后,发起异步请求,把图片先加载下来,最后再插入到页面中的。而在这个插入的过程中,是给了Img.src=””赋值,而这个动作非常关键,因为一旦执行,页面就会发起http请求,这个时候,因为之前ajax请求的图片资源带有expires头,浏览器就会去判断当前时间是否超过了图片的expires头规定的时间,如果没超过的话,就利用缓存的数据,从达到异步请求的效果。

这方面的书籍也很多,推荐《Ajax in action》、《Head.First.Ajax》.

页面组件分离,分摊到多个域名

浏览器为了保护用户的正常使用,对单个页面的并行下载数量是有限制的。比如在桌面版的firefox,并行下载数量就是8个,当然这个我们可以通过about:config那边去进行设置。

对于这种限制,我们可以通过把组件分离,分发到不同的域名服务器下面,从而绕过这个限制。当然了,这个对于静态文件还是比较好做的,但对于动态生成的文件就不好弄了。

HTTP降级

HTTP1.1中建议主机最大并行下载4个组件,而HTTP1.0协议的话,好像不存在这样的情况。那我们要做的就是,对http1.1的请求进行降级,绕过这个协议的限制。