煮酒论坛

 找回密码
 申请新用户
搜索
热搜: 活动 交友 discuz
查看: 11919|回复: 10

高性能WEB开发

[复制链接]
发表于 2010-6-4 00:15:20 | 显示全部楼层 |阅读模式
转自:http://www.blogjava.net/BearRui/archive/2010/04/26/web_performance.html

新产品为了效果,做的比较炫,用了很多的图片和JS,所以前端的性能是很大的问题,分篇记录前端性能优化的一些小经验。

  第一篇:HTTP服务器

  因tomcat处理静态资源的速度比较慢,所以首先想到的就是把所有静态资源(JS,CSS,image,swf)提到单独的服务器,用更加快速的HTTP服务器,这里选择了nginx 了,nginx相比apache,更加轻量级,配置更加简单,而且nginx不仅仅是高性能的HTTP服务器,还是高性能的反向代理服务器。

  目前很多大型网站都使用了nginx,新浪、网易、QQ等都使用了nginx,说明nginx的稳定性和性能还是非常不错的。

  1. nginx 安装(linux)

  http://nginx.org/en/download.html 下载最新稳定版本

  根据自己需要的功能先下载对应模板,这里下载了下面几个模块:

      openssl-0.9.8l,zlib-1.2.3,pcre-8.00

  编译安装nginx:

./configure

--without-http_rewrite_module

--with-http_ssl_module

--with-openssl=../../lib/openssl-0.9.8l

--with-zlib=../../lib/zlib-1.2.3

--with-pcre=../../lib/pcre-8.00

--prefix=/usr/local/nginx

make

make install


2、nginx处理静态资源的配置

  #启动GZIP压缩CSS和JS

  gzip  on;

  # 压缩级别 1-9,默认是1,级别越高压缩率越大,当然压缩时间也就越长

  gzip_comp_level 4;        

  # 压缩类型

  gzip_types text/css application/x-javascript;

  # 定义静态资源访问的服务,对应的域名:res.abc.com

  server {

        listen       80;

        server_name  res.abc.com;

  # 开启服务器读取文件的缓存,

open_file_cache max=200 inactive=2h;

open_file_cache_valid 3h;

open_file_cache_errors off;

charset utf-8;


# 判断如果是图片或swf,客户端缓存5天

location ~* ^.+.(ico|gif|bmp|jpg|jpeg|png|swf)$ {
root /usr/local/resource/;
access_log off;
index index.html index.htm;
expires 5d;
}


# 因JS,CSS改动比较频繁,客户端缓存8小时

location ~* ^.+.(js|css)$ {
root /usr/local/resource/;
access_log off;
index index.html index.htm;
expires 8h;
}


# 其他静态资源

location / {
root /usr/local/resource;
access_log off;
expires 8h;
}
}


    3、nginx 反向代理设置

    # 反向代理服务,绑定域名www.abc.com

server {
listen 80;
server_name www.abc.com;
charset utf-8;
}


  # BBS使用Discuz!

  # 因反向代理为了提高性能,一部分http头部信息不会转发给后台的服务器,

  # 使用proxy_pass_header 和 proxy_set_header 把有需要的http头部信息转发给后台服务器

location ^~ /bbs/ {
root html;
access_log off;
index index.php;
}


   # 转发host的信息,如果不设置host,在后台使用request.getServerName()取到的域名不是www.abc.com,而是 127.0.0.1

   proxy_set_header Host $host;

   # 因Discuz! 为了安全,需要获取客户端User-Agent来判断每次POST数据是否跟第一次请求来自同1个浏览器,

   # 如果不转发User-Agent,Discuz! 提交数据就会报"您的请求来路不正确,无法提交"的错误

proxy_pass_header User-Agent;
proxy_pass http://127.0.0.1:8081;
}


  # 其他请求转发给tomcat

location / {
root html;
access_log off;
index index.jsp;
proxy_pass http://127.0.0.1:8080;
}


error_page 500
502
503
504 /50x.html;

location = /50x.html {

root html;

}

}


  nginx详细配置参考:http://wiki.nginx.org/

 楼主| 发表于 2010-6-4 00:32:26 | 显示全部楼层

web性能测试工具推荐

WEB性能测试工具主要分为三种,一种是测试页面资源加载速度的,一种是测试页面加载完毕后页面呈现、JS操作速度的,还有一种是总体上对页面进行评价分析,下面分别对这些工具进行介绍,如果谁有更好的工具也请一起分享下。

  Firebug:

  Firebug 是firefox中最为经典的开发工具,可以监控请求头,响应头,显示资源加载瀑布图:

  HttpWatch  :

  httpwatch 功能类似firebug,可以监控请求头,响应头,显示资源加载瀑布图。但是httpwatch还能显示GZIP压缩信息,DNS查询,TCP链接信息,个人在监控http请求比较喜欢使用httpwatch,httpwatch包含IE和firefox插件。不过httpwatch专业版本是收费的,免费版本有些功能限制。


  DynaTrace's Ajax Edition:

  dynaTrace 是本人常使用的1个免费工具,该工具不但可以检测资源加载瀑布图,而且还能监控页面呈现时间,CPU花销,JS分析和执行时间,CSS解析时间的等。

  Speed Tracer:

  speed trace 是google chrome的1个插件,speed trace的优势点是用于监控JS的解析执行时间,还可以监控页面的重绘、回流,这个还是很强的(dynaTrace也能有这个功能)。   注:安装这个插件,需要安装 Google Chrome Developer Channel 版本,但是这个链接的地址在国内好像打不开,如果打不开,请大家直接到这个地址去下载:   

http://www.google.com/chrome/eula.html?extra=devchannel

  Page Speed :

  Page speed 是基于firebug的1个工具,主要可以对页面进行评分,总分100分,而且会显示对各项的改进意见,Page Speed也能检测到JS的解析时间。

  yslow :

  yslow跟pge speed一样是基于 firefox\firebug的插件,功能与page speed类似,对各种影响网站性能的因素进行评分,yslow是yahoo的工具,本人也一直在使用,推荐一下。

  webpagetest :

  webpagetest 是1个在线进行性能测试的网站,在该网站输入你的url,就会生成1个url加载的时间瀑布图,对所有加载的资源(css,js,image等等)列出优化的清单,也是非常好用的工具。

 楼主| 发表于 2010-6-4 00:35:38 | 显示全部楼层

图片篇

缩小图片大小

当图片很多的时候,减少图片大小是提高下载速度最直接的方法。
  1. 使用PNG8代替GIF(非动画图片),因为PNG8在效果一样的情况,图片大小比GIF要小。

  2. 用fireworks处理PNG图片,在我们产品中很多PNG图片是美工直接用photoshop导出的,后来让美工用fireworks处理PNG(大概的方式是选择保存为PNG8,删除背景色)。处理后100K的图片大小基本减少了3/4,但图片质量也会有少许降低,要看自己是否能接受。

  3. 使用Smush.it(http://www.smushit.com/ysmush.it/) 压缩图片,Smush.it是YUI团队做1个在线压缩图片的网站,该网站在不影响原图片的质量下去掉图片中一些元数据,所以可以放心使用该网站进行压缩,但这个压缩比例也是比较有限的。

  合并图片和拆分图片

  1. CSS Sprites合并图片以减少请求数来提高性能大家都知道。但不要把图片合并太多,太多太大了,就会因为这1个图片影响这个页面的显示了。

  2. 有时候我们需要把1个大图片拆分成多个小图片,比如产品首页图片比较少,就1个很大的banner图片,因浏览器都可以并发下载图片,所以如果不拆分,只使用1个大图片的话,下载速度反而会比较慢。

  透明图片处理

  IE6不能显示透明的PNG图片,是很多开发人员特别头疼的事,分别介绍下几种方式的优缺点。

  1.使用AlphaImageLoader,IE6支持filter,使用下面的CSS代码,可以让IE6支持PNG

#some-element {
background: url(image.png);
_background: none;
_filter:progidXImageTransform.Microsoft.AlphaImageLoader(src='image.png',
sizingMethod='crop');
}


  优点:使用简单

  缺点:性能损耗很大,AlphaImageLoader会花费很多资源去处理透明图片,使用AlphaImageLoader,IE使用内存会迅速上升。而且AlphaImageLoader所有处理都在同1个线程中同步进行,所以当AlphaImageLoader多的时候,会阻塞UI的渲染。
  使用_filter,IE7也可以识别,其实IE7是可以识别PNG透明图片的,如果在IE7下使用上面代码,IE7不会直接使用图片,而是使用 AlphaImageLoader。
  注:个人建议尽量避免使用 AlphaImageLoader

  2. JS处理
  使用DD_belatedPNG(http://www.dillerdesign.com/experiment/DD_belatedPNG/),可以很简单的对界面上所有的透明图片进行同一处理。优点:使用简单(比AlphaImageLoader还简单)

  缺点:当页面上需要处理的图片比较多的时候,速度也比较慢,而且不能动态改变图片。

  3. VML

  IE6支持VML,VML可以使用透明图片,代码如下:

代码<html xmlns ="http://www.w3.org/1999/xhtml" xmlns>
<head >
<style type ="text/css" >
{v\:* { behavior : url(#default#VML) ;}
<span style="color: rgb(128, 0, 0);"/></style >
<span style="color: rgb(128, 0, 0);"</></head >
<body >
v:image src ="image.png" />
<span style="color: rgb(128, 0, 0);"></body >
<span style="color: rgb(128, 0, 0);"></html >



  优点:性能好,速度快

  缺点:使用复杂,而且不支持firefox等浏览器,需要判断不同的浏览器输出不同的HTML代码。

  多域名下载图片

  因每个浏览器对同1个域名同时只能发送固定的请求,比如IE6好像是2个,所以可以对图片资源开通多个域名进行请求,
比如img1.abc.com,img2.abc.com。但域名不要开启太多,因为解析域名和打开新的连接都需要消耗时间,域名多了,说不定反而会更慢。一般2-4个域名就够了。

  IE6下缓存背景图片

  IE6背景图片缓存是个麻烦事,很多人知道使用下面的JS来让IE6缓存背景图片

try{
document.execCommand("BackgroundImageCache", false, true);
}catch(e){}


  但是这样做的效果并不是非常好,当出现鼠标移动改变背景图片的时候,IE6老是会发送1个图片请求(尽管该背景图片已经下载),虽然返回结果是304,但还是要花费不少时间。在这种情况下,可以使用下面1个变通的方式来处理,在页面上直接使用1个DIV元素来加载该图片,这样加载图片就能真正被缓存,鼠标移动也不会发送请求了。


预加载图片

  使用下面代码可以在页面加载完毕后预加载下1个页面的图片,当进入下1个页面就不用再下载图片了。


window.onload=function(){
var img =
new Image();
img.src =
"images/image.png";
img =
null;
};




 楼主| 发表于 2010-6-4 00:37:25 | 显示全部楼层

如何加载JS,JS应该放在什么位置?

外部JS的阻塞下载

  所有浏览器在下载JS的时候,会阻止一切其他活动,比如其他资源的下载,内容的呈现等等。至到JS下载、解析、执行完毕后才开始继续并行下载其他资源并呈现内容。

  有人会问:为什么JS不能像CSS、image一样并行下载了?这里需要简单介绍一下浏览器构造页面的原理,当浏览器从服务器接收到了HTML文档,并把HTML在内存中转换成DOM树,在转换的过程中如果发现某个节点(node)上引用了CSS或者 IMAGE,就会再发1个request去请求CSS或image,然后继续执行下面的转换,而不需要等待request的返回,当request返回后,只需要把返回的内容放入到DOM树中对应的位置就OK。但当引用了JS的时候,浏览器发送1个js request就会一直等待该request的返回。因为浏览器需要1个稳定的DOM树结构,而JS中很有可能有代码直接改变了DOM树结构,比如使用 document.write 或 appendChild,甚至是直接使用的location.href进行跳转,浏览器为了防止出现JS修改DOM树,需要重新构建DOM树的情况,所以就会阻塞其他的下载和呈现。

  阻塞下载图:下图是访问blogjava首页的时间瀑布图,可以看出来开始的2个image都是并行下载的,而后面的2个JS都是阻塞下载的(1个1个下载)。

  嵌入JS的阻塞下载

  嵌入JS是指直接写在HTML文档中的JS代码。上面说了引用外部的JS会阻塞其后的资源下载和其后的内容呈现,哪嵌入的JS又会是怎样阻塞的了,看下面的列2个代码:


代码<div>
<ul>
<li>blogjava<span style="color: #800000;"/></li>
<li>CSDN<span style="color: #800000;"/></li>
<li>博客园<span style="color: #800000;"/></li>
<li>ABC<span style="color: #800000;"/></li>
<li>AAA<span style="color: #800000;"/></li>
</ul>
</div>
<script type="text/javascript">
// 循环5秒钟
{ var n = Number(new Date());
var n2 = Number(new Date());
while((n2 - n) (6*1000)){
n2 = Number(new Date());
}
</script>
<div>
<ul>
<li>MSN</li>
<li>GOOGLE</li>
<li>YAHOO</li>
</ul>
</div>



代码2(test.zip里面的代码与代码1的JS代码一模一样):会阻塞其他的下载和呈现。

代码<div>
<ul>
<li>blogjava</li>
<li>CSDN</li>
<li>博客园</li>
<li>ABC</li>
<li>AAA</li>
</ul>
</div>
<script type="text/javascript" src="http://www.blogjava.net/Files/BearRui/test.zip"></script>
<div>
<ul>
<li>MSN</li>
<li>GOOGLE</li>
<li>YAHOO</li>
</ul>
</div>



运行后,会发现代码1中,在前5秒中页面上是一篇空白,5秒中后页面全部显示。 代码2中,前5秒中blogjava,csdn等先显示出来,5秒后MSN才显示出来。


  可以看出嵌入JS会阻塞所有内容的呈现,而外部JS只会阻塞其后内容的显示,2种方式都会阻塞其后资源的下载。

  嵌入JS导致CSS阻塞加载的问题

  CSS怎么会阻塞加载了?CSS本来是可以并行下载的,在什么情况下会出现阻塞加载了(在测试观察中,IE6下CSS都是阻塞加载,下面的测试在非IE6下进行):

  代码1(为了效果,这里选择了1个国外服务器的CSS):


<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>js test</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"
/>
<link type="text/css" rel="stylesheet" href="http://69.64.92.205/Css/Home3.css"
/>
</head>
<body>
<img src="http://www.blogjava.net/images/logo.gif"
/><br />
<img src="http://csdnimg.cn/www/images/csdnindex_piclogo.gif"
/>
</body>
</html>


时间瀑布图:


  从时间瀑布图中可以看出,代码2中,CSS和图片并没有并行下载,而是等待 CSS下载完毕后才去并行下载后面的2个图片,当CSS后面跟着嵌入的JS的时候,该CSS就会出现阻塞后面资源下载的情况。

  有人可能会问,这里为什么不说说嵌入的JS阻塞了后面的资源,而是说CSS阻塞了? 想想我们现在用的是1个空函数,解析这个空函数1ms就够,而后面2个图片是等CSS下载完1.3s后才开始下载。大家还可以试试把嵌入JS放到CSS前面,就不会出现阻塞的情况了。

  根本原因:因为浏览器会维持html中css和js的顺序,样式表必须在嵌入的JS执行前先加载、解析完。而嵌入的JS会阻塞后面的资源加载,所以就会出现上面CSS阻塞下载的情况。

  嵌入JS应该放在什么位置

  1、放在底部,虽然放在底部照样会阻塞所有呈现,但不会阻塞资源下载。   
  2、如果嵌入JS放在head中,请把嵌入JS放在CSS头部。
    3、使用defer
    4、不要在嵌入的JS中调用运行时间较长的函数,如果一定要用,可以用setTimeout来调用
  PS:很多网站喜欢在head中嵌入JS,并且习惯放在CSS后面,比如看到的www.qq.com,当然也有很多网站是把JS放到CSS前面的,比如 yahoo,google

 楼主| 发表于 2010-6-4 00:38:28 | 显示全部楼层

为什么要减少请求数,如何减少请求数

http请求头的数据量


  我们先分析下请求头,看看每次请求都带了那些额外的数据.下面是监控的google的请求头


代码Host www.google.com.hk
User-Agent Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.9.2.3) Gecko/20100401
Firefox/3.6.3 GTBDFff GTB7.0
Accept text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language zh-cn,en-us;q=0.7,en;q=0.3
Accept-Encoding gzip,deflate
Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive 115
Proxy-Connection keep-alive



  返回的response head



Date Sat, 17 Apr 2010 08:18:18 GMT
Expires -1
Cache-Control private, max-age=0
Content-Type text/html; charset=UTF-8
Set-Cookie PREF=ID=b94a24e8e90a0f50:NW=1:TM=1271492298M=1271492298:S=JH7CxsIx48Zoo8Nn;
expires=Mon, 16-Apr-2012 08:18:18 GMT; path=/; domain=.google.com.hk NID=33=EJVyLQBv2CSgp
XQTq8DLIT2JQ4aCAE9YKkU2x-h4hVw_ATrGx7njA69UUBMbzVHVnkAOe_jlGGzOoXhQACSFDP1i53C8hWjRTJd0vYtRN
WhGYGv491mwbngkT6LCYbvg; expires=Sun, 17-Oct-2010 08:18:18 GMT; path=/; domain=.google.com.
hk; HttpOnly
Content-Encoding gzip
Server gws
Content-Length 4344


  这里发送的请求头的大小大概420 bytes,返回的请求头大概 600 bytes。可见每次请求都会带上一些额外的信息进行传输(这次请求中还没有带cookie),当请求的资源很小,比如1个不到1k的图标,可能request带的数据比实际图标的数据量还大。所以当请求越多的时候,在网络上传输的数据自然就多,传输速度自然就慢了。其实request自带的数据量还是小问题,毕竟request能带的数据量还是有限的。


  http连接的开销

  相比request头部多余的数据,http连接的开销则更加严重。先看看从用户输入1个URL到下载内容到客户端需要经过哪些阶段:

  1. 域名解析
  2. 开启TCP连接
  3. 发送请求
  4. 等待(主要包括网络延迟和服务器处理时间)
  5. 下载资源

  可能很多人认为每次请求大部分时间都花在下载资源上,让我们看看blogjava资源下载瀑布图(每种颜色代表的阶段与上面5个阶段对应):

  看了上图你可能惊讶,花费在等待阶段的时间比实际下载的时间要多的多,上图告诉我们:

  1. 每次请求花费的大部分时间在其他阶段,而不是在下载资源阶段

  2. 再小的资源照样会花费很多时间在其他阶段,只是下载阶段会比较短(见上图的第6个资源,才284Byte)。

  正对上面提到的2种情况,我们应该要怎么进行优化了?减少请求数来减少其他阶段的花销和网络中传输的数据。

  如何减少请求数

  1、合并文件

  合并文件就是把很多JS文件合并成1个文件,很多CSS文件合并成1个文件,这种方法应该很多人用到过,这里不做详细介绍,只推荐1个合并的工具:yuiCompressor 这个工具yahoo提供的。 http://developer.yahoo.com/yui/compressor/

  2、合并图片

  这是利用css sprite,通过控制背景图片的位置来显示不同的图片。这种技术也是大家都用过的,不做详细介绍,推荐1个在线合并图片的网站:http://csssprites.com/

  3、把JS、CSS合并到1个文件

  上面第1种方法说的只是把几个JS文件合并成1个JS文件,几个CSS文件合并成1个CSS文件,哪如何把CSS和JS都合并到1个文件中,见我的另1篇文章:http://www.blogjava.net/BearRui/ ... /combin_css_js.html

  4、使用Image maps

  Image maps 是把多个图片合并成1个图片,然后使用html中的<map>标签连接图片,并实现点击图片不同的区域执行不同的动作,image map在导航条中比较容易使用到。image map的使用方法见:http://www.w3.org/TR/html401/struct/objects.html#h-13.6

  5、data嵌入图片

  这种方法把图片进行编码直接嵌入到html中进行使用,以减少HTTP请求,但这个会增加HTML页面的大小,而且这样嵌入的图片不能缓存。见下面这个图片:     

  上面的图片就是把图片进行base64编码后使用data:嵌入到html中,代码如下(后面的省略了,大家可以查看源代码看):

  <IMG SRC="data:image/gif;base64,/9j/4AAQSkZJRgABAQEAYABgAA/4QAWRXhpZgAASUkqAAgAA......">

  其中google的视频搜索中,搜索出来的视频缩略图就都是使用嵌入的图片的,见下图:

  以上几种方法在都有利有弊,在不同情况下可以选择不同的使用方式,比如使用data嵌入图片虽然减少了请求数,但会增加页面大小。

  所以微软的bing搜索在用户第一次访问的时候使用data嵌入图片,然后后台懒加载真真的图片,以后访问就直接使用缓存的图片,而不使用data。

 楼主| 发表于 2010-6-4 00:39:19 | 显示全部楼层

减少请求,响应的数据量

上一篇中我们说到了 如何减少请求数,这次说说如何减少请求、响应的数据量(即在网络中传输的数据量),减少传输的数据量不仅仅可以加快页面加载速度,更可以节约服务器带宽,为你剩不少钱(好像很多机房托管都是按流量算钱的)。

  GZIP压缩

  gzip是目前所有浏览器都支持的一种压缩格式,IE6需要SP1及以上才支持(别说你还在用IE5,~_~)。gzip可以说是最方便而且也是最大减少响应数据量的1种方法。

  说它方便,是因为你不需要为它写任何额外的代码,只需要在http服务器上加上配置都行了,现在主流的http 服务器都支持gzip,各种服务器的配置这里就不一一介绍(其实是我不知道怎么配),

  nginx的配置可以参考我这篇文章:www.blogjava.net/BearRui/archive ... ormance_server.html

  我们先看看gzip的压缩比率能达到多少,这里用jquery 1.4.2的min和src2个版本进行测试,使用nginx服务器,gzip压缩级别使用的是:

  注意看上图的红色部分,jquery src文件在启用gzip后大小减少了70%

  这张图片可以看出就算是已经压缩过min.js在启用gzip后大小也减少了65%。

  别对图片启用gzip

  在知道了gzip强大的压缩能力后,你是否想对服务器上的所有文件启用gzip了,先让我们看看图片中启用gzip后会是什么情况。

  hoho,1个gif图片经过gzip压缩后反而变大了???这是因为图片本来就是一种压缩格式,gzip不能再进行压缩,反而会添加1额外的头部信息,所以图片会变大。

  在测试过程中,发现jpg的图片经过gzip压缩后会变小,不知道为何,可能跟图片压缩方式有关。不过压缩比率也比较小,所以就算是jpg,建议也不要开启gzip压缩。

  比较适合启用gzip压缩的文件有如下这些:

  1. javascript

  2. CSS

  3. HTML,xml

  4. plain text

  别乱用cookie
  现在几乎没有哪个网站不使用cookie了,可是该怎么使用cookie比较合适了,cookie有几个重要的属性:path(路径),domain(域),expires(过期时间)。浏览器就是根据这3个属性来判断在发送请求的时候是否需要带上这个cookie。

  cookie使用最好的方式,就是当请求的资源需要cookie的时候才带上该cookie。其他任何请求都不带上cookie。但事实上很多人在使用 cookie的时候已经习惯性的设置成:path=/ domain=.domain.com。这样的结果就是不管任何请求都会带上cookie,就算你是请求的图片(img.domain.com)、静态资源服务器(res.domain.com)这些根本不需要cookie的资源,浏览器照样会带上这些没用的cookie。咱们一起来看现实中的1个列子,博客园(www.cnblogs.com):

  先看看博客园的cookie是怎么设置的,下面是firefox查看博客园cookie的截图:
   

  cnblogs总共有5个cookie值,而且全部设置都是  path=/ domain=.cnblogs.com。知道了cookie的设置后,我们再来监控下博客园首页的请求,监控的统计信息如下:

  总请求数:39(其中图片22个,JS7个,css2个)。

  其中js、css、image 主要来自3个静态资源服务器: common.cnblogs.com , pic.cnblogs.com ,static.cnblogs.com

  再看其中1个请求图片()的请求头:

Host static.cnblogs.com
User-Agent Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.9.2.3) Gecko/20100401
Firefox/3.6.3 GTBDFff GTB7.0
Accept image/png,image/*;q=0.8,*/*;q=0.5
Accept-Language zh-cn,en-us;q=0.7,en;q=0.3
Accept-Encoding gzip,deflate
Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive 115
Proxy-Connection keep-alive
Referer http://www.cnblogs.com/
Cookie __gads=ID=a15d7cb5c3413e56:T=1272278620:S=ALNI_MZNMr6_d_PCjgkJNJeEQXkmZ3bxTQ;
__utma=226521935.1697566422.1272278366.1272278366.1272278366.1;
__utmb=226521935.2.10.1272278366; __utmc=226521935;
__utmz=226521935.1272278367.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none)


我们发现在请求banner_job.gif这个图片的时候,浏览器把cnblogs.com的所有cookie都带上了(其他图片的请求都是一样的),我估计博客园在处理图片的时候应该不需要用到cookie吧?也许你认为这几个cookie的大小只有300个字节左右,无所谓啦。


  我们做个简单的计算,假设博客园每天有50W个PV(实际情况应该不止吧),每次PV大概有15次请求静态资源,15*500000*300/1024 /1024=2145M。也就说这几个cookie每天大概会耗费博客园2G的带宽。当然这种简单的计算方式肯定会有偏差,毕竟我们还没把静态资源缓存考虑进去。但是个人觉得要是博客园要是把cookie的domain设置为www.cnblogs.com会更好一些。

  妙用204状态

  http中200,404,500状态大家都很清楚,但204状态大家可能用的比较少,204状态是指服务器成功处理了客户端请求,但服务器无返回内容。204是HTTP中数据量最少的响应状态,204的响应中没有body,而且Content-Length=0。很多人在使用ajax提交一些数据给服务器,而不需要服务器返回的时候,常常在服务端使用下面的代码:response.getWriter().print(""),这是返回1个空白的页面,是1个200请求。它还是有body,而且Content-Length不会等于0。其实这个时候你完全可以直接返回1个204状态 (response.setStatus(204))。204在一些网站分析的代码中最常用到,只需要把客户端的一些信息提交给服务器就完事,让我们看看 google首页的1个204响应,google首页的最后1个请求返回的就是204状态,但这个请求是干嘛用的就没猜出来了:

 楼主| 发表于 2010-6-4 00:40:07 | 显示全部楼层

JS、CSS的合并、压缩、缓存管理

存在的问题:   

      合并、压缩文件主要有2方面的问题:

       1. 每次发布的时候需要运行一下自己写的bat文件或者其他程序把文件按照自己的配置合并和压缩。

       2. 因生产环境和开发环境需要加载的文件不一样,生产环境为了需要加载合并、压缩后的文件,而开发环境为了修改、调试方便,需要加载非合并、压缩的文件,所以我们常常需要在JSP中类似与下面的判断代码:
<c:if test="${env=='prod'}">
   <script type="text/javascript" src="/js/all.js"></script>
</c:if>
<c:if test="${env=='dev'}">
   <script type="text/javascript" src="/js/1.js"></script>
   <script type="text/javascript" src="/js/2.js"></script>
   <script type="text/javascript" src="/js/3.js"></script>
</c:if>
         缓存问题:在现在JS满天飞的时代,大家都知道缓存能带来的巨大好处,但缓存确实非常麻烦的一个问题,相信很多人曾经历过下面的情况:为了让程序更快,在服务器上为JS加上缓冲5天的代码,但产品更新后第二天就接到电话说系统出错,详细了解后就发现是缓存引起的,让用户删除缓存后就会OK。原因很简单,就是你JS已经修改了,但用户还在使用缓存中的老JS。在经历几次这种情况,被领导数落了几次后。没办法只能把JS的缓冲去掉,或者改成8个小时。可这样就完全失去了缓存的优势了,哪我们到底需要解决哪些问题才能让我们使用缓冲顺心如意了?
    1. 如何在修改了某个JS后,自动把所有引用该JS页面的代码中加上1个版本号?

    2. 该如何生成版本号,根据什么来产生这个版本号。

    可能有人为了解决上面的缓存问题,写了个JSP标签,通过标签读取JS、css文件的修改时间来作为版本号,从而来解决上面2个问题。但这种方法有下面几个缺点:
    1. 每次请求都要通过标签读取读取文件的修改时间,速度慢。当然你可以把文件的修改时间放到缓存中,这样也会加到了内存使用量。

    2. 在HTML静态页面中用不了

    3. 如果你们公司是如下的部署发布方式(我们公司就是这样),则会失效。每次发布,不是直接覆盖之前的WEB目录,运维的为的发布方便,要求每次发布直接给他们1个war包,他们会把之前WEB目录整个删除,然后上传现在的war包,这样就导致程序运行后,所有文件的最后修改时间都是解压war的时间。


分享自己项目中的处理方案:

    为了解决上面讨论过的问题,在下写了1个如下的组件,组件中根据我们自己的实际情况使用了文件大小来做为文件的版本号,虽然在文件修改很小(比如把字符a改成b),可能文件大小并没有变,导致版本号也不会变。

但这种机率还是非常低的。当然如果你觉的使用文件修改时间作为版本号适合你,只需要修改一行代码就行,下面看下这个组件的处理流程(本来想用流程图表达,最后还是觉的文字来的直白写):

    1. 程序启动(contextInitialized)

    2. 搜索程序目录下的所有merge.txt文件,根据merge.txt文件的配置合并文件, merge.txt文件实例如下:
# 文件合并配置文件,多个文件以|隔开,以/开头的表示从根目录开始,
# 空格之后的文件名表示合并之后的文件名

# 把1,2,3合并到all文件中
1.js|2.js|3.js all.js

#合并CSS
/css/mian.css|/css/common.css all.css

    3. 搜索程序目录下所有JS,CSS文件(包括合并后的),每个文件都压缩后生成对应的1个新文件。


    4. 搜索程序目录下所有JSP,html文件,把所有JS,css的引用代码改成压缩后并加了版本号的引用。


实例:

    实例的文件结构如下图:
   

   
    看JSP原始代码(程序运行前):
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"  "http://www.w3.org/TR/html4/loose.dtd">
<% boolean isDev = false;  // 是否开发环境%>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>JSP Page</title>
        <% if(isDev){ %>
        <script type="text/javascript" src="<%=request.getContextPath() %>/js/jquery-1.4.2.js"></script>
        <script type="text/javascript" src="<%=request.getContextPath() %>/js/1.js"></script>
        <script type="text/javascript" src="<%=request.getContextPath() %>/js/2.js"></script>
        <link type="text/css" rel="stylesheet" href="<%=request.getContextPath() %>/css/1.css" />
        <link type="text/css" rel="stylesheet" href="<%=request.getContextPath() %>/css/2.css" />
        <% }else{ %>
        <script type="text/javascript" src="<%=request.getContextPath() %>/js/jquery-1.4.2.js"></script>
        <script type="text/javascript" src="<%=request.getContextPath() %>/js/all.js"></script>
        <link type="text/css" rel="stylesheet"  href="<%=request.getContextPath() %>/css/all.css" />
        <% } %>
    </head>
    <body>
        <h1 class="c1">Hello World!</h1>
    </body>
</html>


    程序运行后JSP的代码:

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%
    boolean isDev = false;  // 是否开发环境
%>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>JSP Page</title>
        <% if(isDev){ %>
        <script type="text/javascript" src="<%=request.getContextPath() %>/js/jquery-1.4.2-3gmin.js?99375"></script>
        <script type="text/javascript" src="<%=request.getContextPath() %>/js/1-3gmin.js?90"></script>
        <script type="text/javascript" src="<%=request.getContextPath() %>/js/2-3gmin.js?91"></script>
        <link type="text/css" rel="stylesheet" href="<%=request.getContextPath() %>/css/1-3gmin.css?35" />
        <link type="text/css" rel="stylesheet" href="<%=request.getContextPath() %>/css/2-3gmin.css?18" />
        <% }else{ %>
        <script type="text/javascript" src="<%=request.getContextPath() %>/js/jquery-1.4.2-3gmin.js?99375"></script>
        <script type="text/javascript" src="<%=request.getContextPath() %>/js/all-3gmin.js?180"></script>
        <link type="text/css" rel="stylesheet"  href="<%=request.getContextPath() %>/css/all-3gmin.css?53" />
        <% } %>
    </head>
    <body>
        <h1 class="c1">Hello World!</h1>
    </body>
</html>

    加3gmin后缀的文件全部是程序启动时自动生成的。

实例下载:猛击此处下载

PS:自己的设计的处理方案并没有解决"需要JSP中加判断代码的问题",主要是因为还没有找到什么好的办法去自动删除1.js,2.js,3.js的3个引用,而插入1个新的all.js的引用,如果那位同学对解决这个问题有好的想法,请不吝赐教。
      如果有同学想使用这个组件,建议在测试环境下运行一次后,把修改后的程序直接上传到正式服务器上,然后去掉这个功能,不然在服务器上每次启动都调用这个功能还是需要花费一些时间和资源的  
       其实一直想使用SVN中的版本号来控制缓存,这个是最严谨的一个方法,但也因为做起来太复杂,所以一直也没做起来,以后以后有时间可以再研究。

 楼主| 发表于 2010-6-4 00:40:50 | 显示全部楼层

页面呈现、重绘、回流

页面呈现流程

     在讨论页面重绘、回流之前。需要对页面的呈现流程有些了解,页面是怎么把html结合css等显示到浏览器上的,下面的流程图显示了浏览器对页面的呈现的处理流程。可能不同的浏览器略微会有些不同。但基本上都是类似的。
     



     1.  浏览器把获取到的html代码解析成1个Dom树,html中的每个tag都是Dom树中的1个节点,根节点就是我们常用的document对象(<html> tag)。dom树就是我们用firebug或者IE Developer Toolbar等工具看到的html结构,里面包含了所有的html tag,包括display:none隐藏,还有用JS动态添加的元素等。

     2. 浏览器把所有样式(主要包括css和浏览器的样式设置)解析成样式结构体,在解析的过程中会去掉浏览器不能识别的样式,比如IE会去掉-moz开头的样式,而firefox会去掉_开头的样式。

     3、dom tree和样式结构体结合后构建呈现树(render tree),render tree有点类似于dom tree,但其实区别有很大,render tree能识别样式,render tree中每个node都有自己的style,而且render tree不包含隐藏的节点(比如display:none的节点,还有head节点),因为这些节点不会用于呈现,而且不会影响呈现的,所以就不会包含到render tree中。注意 visibility:hidden隐藏的元素还是会包含到render tree中的,因为visibility:hidden 会影响布局(layout),会占有空间。根据css2的标准,render tree中的每个节点都称为box(Box dimensions),box所有属性:width,height,margin,padding,left,top,border等。

    4. 一旦render tree构建完毕后,浏览器就可以根据render tree来绘制页面了。

回流与重绘

    1. 当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(其实我觉得叫重新布局更简单明了些)。每个页面至少需要一次回流,就是在页面第一次加载的时候。

    2. 当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。则就叫称为重绘。

  注:从上面可以看出,回流必将引起重绘,而重绘不一定会引起回流。


什么操作会引起重绘、回流

    其实任何对render tree中元素的操作都会引起回流或者重绘,比如:

    1. 添加、删除元素(回流+重绘)

    2. 隐藏元素,display:none(回流+重绘),visibility:hidden(只重绘,不回流)

    3. 移动元素,比如改变top,left(jquery的animate方法就是,改变top,left不一定会影响回流),或者移动元素到另外1个父元素中。(重绘+回流)

    4. 对style的操作(对不同的属性操作,影响不一样)

    5. 还有一种是用户的操作,比如改变浏览器大小,改变浏览器的字体大小等(回流+重绘)


    让我们看看下面的代码是如何影响回流和重绘的: var s = document.body.style;

s.padding
= "2px"; // 回流+重绘
s.border = "1px solid red"; // 再一次 回流+重绘

s.color = "blue"; // 再一次重绘
s.backgroundColor = "#ccc"; // 再一次 重绘

s.fontSize = "14px"; // 再一次 回流+重绘

// 添加node,再一次 回流+重绘
document.body.appendChild(document.createTextNode('abc!'));

   请注意我上面用了多少个再一次。

   说到这里大家都知道回流比重绘的代价要更高,回流的花销跟render tree有多少节点需要重新构建有关系,假设你直接操作body,比如在body最前面插入1个元素,会导致整个render tree回流,这样代价当然会比较高,但如果是指body后面插入1个元素,则不会影响前面元素的回流。

聪明的浏览器

     从上个实例代码中可以看到几行简单的JS代码就引起了6次左右的回流、重绘。而且我们也知道回流的花销也不小,如果每句JS操作都去回流重绘的话,浏览器可能就会受不了。所以很多浏览器都会优化这些操作,浏览器会维护1个队列,把所有会引起回流、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会把flush队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。

    虽然有了浏览器的优化,但有时候我们写的一些代码可能会强制浏览器提前flush队列,这样浏览器的优化可能就起不到作用了。当你请求向浏览器请求一些style信息的时候,就会让浏览器flush队列,比如:
    1. offsetTop, offsetLeft, offsetWidth, offsetHeight
    2. scrollTop/Left/Width/Height
    3. clientTop/Left/Width/Height
    4. width,height
    5. 请求了getComputedStyle(), 或者 ie的 currentStyle
   
    当你请求上面的一些属性的时候,浏览器为了给你最精确的值,需要flush队列,因为队列中可能会有影响到这些值的操作。

如何减少回流、重绘

    减少回流、重绘其实就是需要减少对render tree的操作,并减少对一些style信息的请求,尽量利用好浏览器的优化策略。具体方法有:


    1. 不要1个1个改变元素的样式属性,最好直接改变className,但className是预先定义好的样式,不是动态的,如果你要动态改变一些样式,则使用cssText来改变,见下面代码:
// 不好的写法
var left = 1;
var top = 1;
el.style.left
= left + "px";
el.style.top  
= top  + "px";

// 比较好的写法
el.className += " className1";

// 比较好的写法
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";


    2. 让要操作的元素进行"离线处理",处理完后一起更新,这里所谓的"离线处理"即让元素不存在于render tree中,比如:
        a) 使用documentFragment或div等元素进行缓存操作,这个主要用于添加元素的时候,大家应该都用过,就是先把所有要添加到元素添加到1个div(这个div也是新加的),
            最后才把这个div append到body中。
        b) 先display:none 隐藏元素,然后对该元素进行所有的操作,最后再显示该元素。因对display:none的元素进行操作不会引起回流、重绘。所以只要操作只会有2次回流。

   3 不要经常访问会引起浏览器flush队列的属性,如果你确实要访问,就先读取到变量中进行缓存,以后用的时候直接读取变量就可以了,见下面代码:
// 别这样写,大哥
for(循环) {
    el.style.left
= el.offsetLeft + 5 + "px";
    el.style.top  
= el.offsetTop  + 5 + "px";
}

// 这样写好点
var left = el.offsetLeft,top  = el.offsetTop,s = el.style;
for(循环) {
    left
+= 10;
    top  
+= 10;
    s.left
= left + "px";
    s.top  
= top  + "px";
}


    4. 考虑你的操作会影响到render tree中的多少节点以及影响的方式,影响越多,花费肯定就越多。比如现在很多人使用jquery的animate方法移动元素来展示一些动画效果,想想下面2种移动的方法:
// block1是position:absolute 定位的元素,它移动会影响到它父元素下的所有子元素。

// 因为在它移动过程中,所有子元素需要判断block1的z-index是否在自己的上面,
// 如果是在自己的上面,则需要重绘,这里不会引起回流
$("#block1").animate({left:50});

// block2是相对定位的元素,这个影响的元素与block1一样,但是因为block2非绝对定位
// 而且改变的是marginLeft属性,所以这里每次改变不但会影响重绘,
// 还会引起父元素及其下元素的回流

$("#block2").animate({marginLeft:50});


实例测试

    最后用2个工具对上面的理论进行一些测试,这2个工具是在我 "web 性能测试工具推荐" 文章中推荐过的工具,分别是:dynaTrace(测试ie),Speed Tracer(测试Chrome)。

    第一个测试代码不改变元素的规则,大小,位置。只改变颜色,所以不存在回流,仅测试重绘,代码如下:
<body>
    <script type="text/javascript">

var s = document.body.style;
        
var computed;
        
if (document.body.currentStyle) {
          computed
= document.body.currentStyle;
        }
else {
          computed
= document.defaultView.getComputedStyle(document.body, '');
        }
   
function testOneByOne(){
      s.color
= 'red';;
      tmp
= computed.backgroundColor;
      s.color
= 'white';
      tmp
= computed.backgroundImage;
      s.color
= 'green';
      tmp
= computed.backgroundAttachment;
    }
   
   
function testAll() {
      s.color
= 'yellow';
      s.color
= 'pink';
      s.color
= 'blue';
      
      tmp
= computed.backgroundColor;
      tmp
= computed.backgroundImage;
      tmp
= computed.backgroundAttachment;
    }
   
</script>   
    color test
<br />
    <button onclick="testOneByOne()">Test One by One</button>
    <button onclick="testAll()">Test All</button>
</body>        

    testOneByOne 函数改变3次color,其中每次改变后调用getComputedStyle,读取属性值(按我们上面的讨论,这里会引起队列的flush),testAll 同样是改变3次color,但是每次改变后并不马上调用getComputedStyle

    我们先点击Test One by One按钮,然后点击 Test All,用dynaTrace监控如下:
   
   


    上图可以看到我们执行了2次button的click事件,每次click后都跟一次rendering(页面重绘),2次click函数执行的时间都差不多,0.25ms,0.26ms,但其后的rendering时间就相差一倍多。(这里也可以看出,其实很多时候前端的性能瓶颈并不在于JS的执行,而是在于页面的呈现,这种情况在用JS做到富客户端中更为突出)。我们再看图的下面部分,这是第一次rendering的详细信息,可以看到里面有2行是 Scheduleing layout task,这个就是我们前面讨论过的浏览器优化过的队列,可以看出我们引发2次的flush。
   
   


   再看第二次rendering的详细信息,可以看出并没有Scheduleing layout task,所以这次rendering的时间也比较短。


  测试代码2:这个测试跟第一次测试的代码很类似,但加上了对layout的改变,为的是测试回流。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
</head>
<body>
    <script type="text/javascript">

var s = document.body.style;
        
var computed;
        
if (document.body.currentStyle) {
          computed
= document.body.currentStyle;
        }
else {
          computed
= document.defaultView.getComputedStyle(document.body, '');
        }
   
function testOneByOne(){
      s.color
= 'red';
      s.padding
= '1px';
      tmp
= computed.backgroundColor;
      s.color
= 'white';
      s.padding
= '2px';
      tmp
= computed.backgroundImage;
      s.color
= 'green';
      s.padding
= '3px';
      tmp
= computed.backgroundAttachment;
    }
   
   
function testAll() {
      s.color
= 'yellow';
      s.padding
= '4px';
      s.color
= 'pink';
      s.padding
= '5px';
      s.color
= 'blue';
      s.padding
= '6px';
      
      tmp
= computed.backgroundColor;
      tmp
= computed.backgroundImage;
      tmp
= computed.backgroundAttachment;
    }
   
</script>   
    color test
<br />
    <button onclick="testOneByOne()">Test One by One</button>
    <button onclick="testAll()">Test All</button>
</body>        


   用dynaTrace监控如下:
   


  相信这图不用多说大家都能看懂了吧,可以看出有了回流后,rendering的时间相比之前的只重绘,时间翻了3倍了,可见回流的高成本性啊。
  大家看到时候注意明细处相比之前的多了个 Calcalating flow layout。


  最后再使用Speed Tracer测试一下,其实结果是一样的,只是让大家了解下2个测试工具:

  测试1:
  


  图上第一次点击执行2ms(其中有50% 用于style Recalculation), 第二次1ms,而且第一次click后面也跟了2次style Recalculation,而第二次点击却没有style Recalculation。
  但是这次测试发现paint重绘的时间竟然是一样的,都是3ms,这可能就是chrome比IE强的地方吧。

  测试2:
  

  
  从图中竟然发现第二次的测试结果在时间上跟第一次的完全一样,这可能是因为操作太少,而chrome又比较强大,所以没能测试明显结果出来,
但注意图中多了1个紫色部分,就是layout的部分。也就是我们说的回流。
 楼主| 发表于 2010-6-4 00:41:26 | 显示全部楼层

该如何加载google-analytics(或其他第三方)的JS

很多网站为了获取用户访问网站的统计信息,使用了google-analytics或其他分析网站(下面的讨论中只提google-analytics,简称ga)。注册ga后,ga就会生成一段js脚本,很多人直接把这段js复制到<body>的最后面就完事(包括 博客园、CSDN、BlogJava)。可是ga自动生成的这段JS真的就是最合理的吗?

     哪怎么样才算是合理,怎样才是不合理了?因ga只是1个分析工具,它的使用绝对不能影响到我们的程序,如果影响了,则是不合理的。不影响则是合理的。

目前ga的使用:

     先看看ga自动生成的js脚本,如下:
<script type="text/javascript"> var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));</script> <script type="text/javascript"> try {var pageTracker = _gat._getTracker("UA-123456-1");pageTracker._trackPageview();} catch(err) {}</script>

      看这段代码,使用document.write来加载JS,注意了,这样加载js是阻塞加载的,就是这个js没加载完,后面的所有资源和JS都不能下载和执行。可能你会觉的这段代码在body的最后面,后没已经没内容,没什么会阻塞的了。

     还有一些你忽略了,相信很多人在写JS的时候需要在页面加载完毕后执行一些JS或AJAX,一般写在window.onload 事件,或者写入jquery的$(document).ready()方法中。这些JS就会被阻塞。如果我们的页面上很多数据在window.onload中使用AJAX加载,而偏偏这个时候ga因为某些原因(和谐和谐)不能访问,或者访问很慢的时候。问题就来,我们自己的JS一直在等待ga的JS加载完,只有等ga的js加载超时后才会执行我们的JS。



     实例:
       下面的代码使用jquery在document.ready发送1个ajax请求(请求126.com)。测试前修改host文件,让ga的js无法加载:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script>
    <script type="text/javascript">
        $(document).ready(function(){
            $.get("http://www.126.com/");
        });
  </script>   
</head>
<body>
    <script type="text/javascript">
        var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
        document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
        </script>
        <script type="text/javascript">
        try {
        var pageTracker = _gat._getTracker("UA-123456-1");
        pageTracker._trackPageview();
        } catch(err) {}</script>
</body>
</html>

       监控图:
      

上图可以看出ga加载不了,在20秒超时后,才执行我们的ajax请求,我们的ajax请求才花0.173s,但却等了20s。

合理使用ga:

   要合理使用ga,需要解决2个问题:
      1. 如何非加载ga的js,
      2. 如何在ga的ja加载完毕后立刻执行 var pageTracker = _gat._getTracker("UA-123456-1");pageTracker._trackPageview(); 代码。

   非阻塞加载js的方法,主要有2种:
      1. 动态创建<script标签
      2.使用new Image().src="", 这种方法只会下载JS,而不会解析JS。所以用这个加载js后,里面的函数也不能调用(这种方法一般用于预加载)。

   完善后的代码:

<script type="text/javascript">
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
var head = document.getElementsByTagName("head")[0] || document.documentElement;
var script = document.createElement("script");
script.src = gaJsHost + "google-analytics.com/ga.js";

var done = false; // 防止onload,onreadystatechange同时执行
// 加载完毕后执行,适应所有浏览器
script.onload = script.onreadystatechange = function() {
    if (!done && (!this.readyState || this.readyState === "loaded" || this.readyState === "complete")){
        done = true;
        try {
            var pageTracker = _gat._getTracker("UA-123456-16");
            pageTracker._trackPageview();
        } catch(err) {}
        script.onload = script.onreadystatechange = null;
    }
};
head.insertBefore(script,head.firstChild);
</script>

上面代码修改自jquery的ajax代码。上面代码很容易理解,动态创建script来加载js,通过onload,或 onreadystatechange 事件来加载完毕后执行代码。

代码修改完毕后再监控测试如下;



图中看出ga照样加载了20s,但我们的ajax请求并没有等20s后才执行,而是立刻执行了。
jquery 加载ga:
    可能你觉的上面的代码写的比较多,比较繁琐,如果你用jquery的话,可以简化成下面这样:
    var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");$.getScript(gaJsHost + "google-analytics.com/ga.js",function(){try {var pageTracker = _gat._getTracker("UA-123456-16");pageTracker._trackPageview();} catch(err) {}});
 楼主| 发表于 2010-6-4 00:42:08 | 显示全部楼层

疯狂的HTML压缩

上一篇随笔中网友 skyaspnet 问我如何压缩HTML,当时回答是推荐他使用gzip,后来想想,要是能把所有的html,jsp(aspx)在运行前都压缩成1行未免不是一件好事啊。一般我们启动gzip都比较少对html启动gzip,因为现在的html都是动态的,不会使用浏览器缓存,而启用gzip的话每次请求都需要压缩,会比较消耗服务器资源,对js,css启动gzip比较好是因为js,css都会使用缓存。我个人觉得的压缩html的最大好处就是一本万利,只要写好了一次,以后所有程序都可以使用,不会增加任何额外的开发工作。


     在“JS、CSS的合并、压缩、缓存管理”一文中说到自己写过的1个自动合并、压缩JS,CSS,并添加版本号的组件。这次把压缩html的功能也加入到该组件中,流程很简单,就是在程序启动(contextInitialized or Application_Start)的时候扫描所有html,jsp(aspx)进行压缩。


压缩的注意事项:

     实现的方式主要是用正则表达式去查找,替换。在html压缩的时候,主要要注意下面几点:

          1. pre,textarea 标签里面的内容格式需要保留,不能压缩。

          2. 去掉html注释的时候,有些注释是不能去掉的,比如:<!--[if IE 6]> ..... <![endif]-->

          3. 压缩嵌入式js中的注释要注意,因为可能注释符号会出现在字符串中,比如: var url = "http://www.cnblogs.com";    // 前面的//不是注释

              去掉JS换行符的时候,不能直接跟一下行动内容,需要有空格,考虑下面的代码:

              else

                 return;

             如果不带空格,则变成elsereturn。

          4. jsp(aspx) 中很有可能会使用<% %>嵌入一些服务器代码,这个时候也需要单独处理,里面注释的处理方法跟js的一样。


源代码:

    下面是java实现的源代码,也可以 猛击此处 下载该代码,相信大家都看的懂,也很容易改成net代码:
      1
import java.io.StringReader;

  2 import java.io.StringWriter;
  3 import java.util.*;
  4 import java.util.regex.*;
  5
  6 /*******************************************
  7 * 压缩jsp,html中的代码,去掉所有空白符、换行符
  8 * @author  bearrui(ak-47)
  9 * @version 0.1
10 * @date     2010-5-13
11 *******************************************/
12 public
class HtmlCompressor {
13
private
static String tempPreBlock =
"%%%HTMLCOMPRESS~PRE&&&";
14
private
static String tempTextAreaBlock =
"%%%HTMLCOMPRESS~TEXTAREA&&&";
15
private
static String tempScriptBlock =
"%%%HTMLCOMPRESS~SCRIPT&&&";
16
private
static String tempStyleBlock =
"%%%HTMLCOMPRESS~STYLE&&&";
17
private
static String tempJspBlock =
"%%%HTMLCOMPRESS~JSP&&&";
18
19
private
static Pattern commentPattern = Pattern.compile("<!--\\s*[^\\[].*?-->", Pattern.DOTALL | Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
20
private
static Pattern itsPattern = Pattern.compile(">\\s+?<", Pattern.DOTALL | Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
21
private
static Pattern prePattern = Pattern.compile("<pre[^>]*?>.*?</pre>", Pattern.DOTALL | Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
22
private
static Pattern taPattern = Pattern.compile("<textarea[^>]*?>.*?</textarea>", Pattern.DOTALL | Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
23
private
static Pattern jspPattern = Pattern.compile("<%([^-@][\\w\\W]*?)%>", Pattern.DOTALL | Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
24
// <script></script>
25
private
static Pattern scriptPattern = Pattern.compile("(?:<script\\s*>|<script type=['\"]text/javascript['\"]\\s*>)(.*?)</script>", Pattern.DOTALL | Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
26
private
static Pattern stylePattern = Pattern.compile("<style[^>()]*?>(.+)</style>", Pattern.DOTALL | Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
27
28
// 单行注释,
29
private
static Pattern signleCommentPattern = Pattern.compile("//.*");
30
// 字符串匹配
31
private
static Pattern stringPattern = Pattern.compile("(\"[^\"\\n]*?\"|'[^'\\n]*?')");
32
// trim去空格和换行符
33
private
static Pattern trimPattern = Pattern.compile("\\n\\s*",Pattern.MULTILINE);
34
private
static Pattern trimPattern2 = Pattern.compile("\\s*\\r",Pattern.MULTILINE);
35
// 多行注释
36
private
static Pattern multiCommentPattern = Pattern.compile("/\\*.*?\\*/", Pattern.DOTALL | Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
37
38
private
static String tempSingleCommentBlock =
"%%%HTMLCOMPRESS~SINGLECOMMENT&&&";  //
//占位符
39
private
static String tempMulitCommentBlock1 =
"%%%HTMLCOMPRESS~MULITCOMMENT1&&&";  // /*占位符
40
private
static String tempMulitCommentBlock2 =
"%%%HTMLCOMPRESS~MULITCOMMENT2&&&";  // */占位符
41
42
43
public
static String compress(String html) throws Exception {
44
if(html ==
null
|| html.length() ==
0) {
45
return html;
46         }
47
48         List<String> preBlocks =
new ArrayList<String>();
49         List<String> taBlocks =
new ArrayList<String>();
50         List<String> scriptBlocks =
new ArrayList<String>();
51         List<String> styleBlocks =
new ArrayList<String>();
52         List<String> jspBlocks =
new ArrayList<String>();
53
54         String result = html;
55
56
//preserve inline java code
57         Matcher jspMatcher = jspPattern.matcher(result);
58
while(jspMatcher.find()) {
59             jspBlocks.add(jspMatcher.group(0));
60         }
61         result = jspMatcher.replaceAll(tempJspBlock);
62
63
//preserve PRE tags
64         Matcher preMatcher = prePattern.matcher(result);
65
while(preMatcher.find()) {
66             preBlocks.add(preMatcher.group(0));
67         }
68         result = preMatcher.replaceAll(tempPreBlock);
69
70
//preserve TEXTAREA tags
71         Matcher taMatcher = taPattern.matcher(result);
72
while(taMatcher.find()) {
73             taBlocks.add(taMatcher.group(0));
74         }
75         result = taMatcher.replaceAll(tempTextAreaBlock);
76
77
//preserve SCRIPT tags
78         Matcher scriptMatcher = scriptPattern.matcher(result);
79
while(scriptMatcher.find()) {
80             scriptBlocks.add(scriptMatcher.group(0));
81         }
82         result = scriptMatcher.replaceAll(tempScriptBlock);
83
84
// don't process inline css
85         Matcher styleMatcher = stylePattern.matcher(result);
86
while(styleMatcher.find()) {
87             styleBlocks.add(styleMatcher.group(0));
88         }
89         result = styleMatcher.replaceAll(tempStyleBlock);
90
91
//process pure html
92         result = processHtml(result);
93
94
//process preserved blocks
95         result = processPreBlocks(result, preBlocks);
96         result = processTextareaBlocks(result, taBlocks);
97         result = processScriptBlocks(result, scriptBlocks);
98         result = processStyleBlocks(result, styleBlocks);
99         result = processJspBlocks(result, jspBlocks);
100
101         preBlocks = taBlocks = scriptBlocks = styleBlocks = jspBlocks =
null;
102
103
return result.trim();
104     }
105
106
private
static String processHtml(String html) {
107         String result = html;
108
109
//remove comments
110 //        if(removeComments) {
111             result = commentPattern.matcher(result).replaceAll("");
112 //        }
113
114
//remove inter-tag spaces
115 //        if(removeIntertagSpaces) {
116             result = itsPattern.matcher(result).replaceAll("><");
117 //        }
118
119
//remove multi whitespace characters
120 //        if(removeMultiSpaces) {
121             result = result.replaceAll("\\s{2,}","
");
122 //        }
123
124
return result;
125     }
126
127
private
static String processJspBlocks(String html, List<String> blocks){
128         String result = html;
129
for(int i =
0; i < blocks.size(); i++) {
130             blocks.set(i, compressJsp(blocks.get(i)));
131         }
132
//put preserved blocks back
133
while(result.contains(tempJspBlock)) {
134             result = result.replaceFirst(tempJspBlock, Matcher.quoteReplacement(blocks.remove(0)));
135         }
136
137
return result;
138     }
139
private
static String processPreBlocks(String html, List<String> blocks) throws Exception {
140         String result = html;
141
142
//put preserved blocks back
143
while(result.contains(tempPreBlock)) {
144             result = result.replaceFirst(tempPreBlock, Matcher.quoteReplacement(blocks.remove(0)));
145         }
146
147
return result;
148     }
149
150
private
static String processTextareaBlocks(String html, List<String> blocks) throws Exception {
151         String result = html;
152
153
//put preserved blocks back
154
while(result.contains(tempTextAreaBlock)) {
155             result = result.replaceFirst(tempTextAreaBlock, Matcher.quoteReplacement(blocks.remove(0)));
156         }
157
158
return result;
159     }
160
161
private
static String processScriptBlocks(String html, List<String> blocks) throws Exception {
162         String result = html;
163
164 //        if(compressJavaScript) {
165
for(int i =
0; i < blocks.size(); i++) {
166                 blocks.set(i, compressJavaScript(blocks.get(i)));
167             }
168 //        }
169
170
//put preserved blocks back
171
while(result.contains(tempScriptBlock)) {
172             result = result.replaceFirst(tempScriptBlock, Matcher.quoteReplacement(blocks.remove(0)));
173         }
174
175
return result;
176     }
177
178
private
static String processStyleBlocks(String html, List<String> blocks) throws Exception {
179         String result = html;
180
181 //        if(compressCss) {
182
for(int i =
0; i < blocks.size(); i++) {
183                 blocks.set(i, compressCssStyles(blocks.get(i)));
184             }
185 //        }
186
187
//put preserved blocks back
188
while(result.contains(tempStyleBlock)) {
189             result = result.replaceFirst(tempStyleBlock, Matcher.quoteReplacement(blocks.remove(0)));
190         }
191
192
return result;
193     }
194
195
private
static String compressJsp(String source)  {
196
//check if block is not empty
197         Matcher jspMatcher = jspPattern.matcher(source);
198
if(jspMatcher.find()) {
199             String result = compressJspJs(jspMatcher.group(1));
200
return (new StringBuilder(source.substring(0, jspMatcher.start(1))).append(result).append(source.substring(jspMatcher.end(1)))).toString();
201         } else {
202
return source;
203         }
204     }   
205
private
static String compressJavaScript(String source)  {
206
//check if block is not empty
207         Matcher scriptMatcher = scriptPattern.matcher(source);
208
if(scriptMatcher.find()) {
209             String result = compressJspJs(scriptMatcher.group(1));
210
return (new StringBuilder(source.substring(0, scriptMatcher.start(1))).append(result).append(source.substring(scriptMatcher.end(1)))).toString();
211         } else {
212
return source;
213         }
214     }
215
216
private
static String compressCssStyles(String source)  {
217
//check if block is not empty
218         Matcher styleMatcher = stylePattern.matcher(source);
219
if(styleMatcher.find()) {
220
// 去掉注释,换行
221             String result= multiCommentPattern.matcher(styleMatcher.group(1)).replaceAll("");
222             result = trimPattern.matcher(result).replaceAll("");
223             result = trimPattern2.matcher(result).replaceAll("");
224
return (new StringBuilder(source.substring(0, styleMatcher.start(1))).append(result).append(source.substring(styleMatcher.end(1)))).toString();
225         } else {
226
return source;
227         }
228     }
229
230
private
static String compressJspJs(String source){
231         String result = source;
232
// 因注释符合有可能出现在字符串中,所以要先把字符串中的特殊符好去掉
233         Matcher stringMatcher = stringPattern.matcher(result);
234
while(stringMatcher.find()){
235             String tmpStr = stringMatcher.group(0);
236
237
if(tmpStr.indexOf("//") !=
-1
|| tmpStr.indexOf("/*") !=
-1
|| tmpStr.indexOf("*/") !=
-1){
238                 String blockStr = tmpStr.replaceAll("//", tempSingleCommentBlock).replaceAll("/\\*", tempMulitCommentBlock1)
239                                 .replaceAll("\\*/", tempMulitCommentBlock2);
240                 result = result.replace(tmpStr, blockStr);
241             }
242         }
243
// 去掉注释
244         result = signleCommentPattern.matcher(result).replaceAll("");
245         result = multiCommentPattern.matcher(result).replaceAll("");
246         result = trimPattern2.matcher(result).replaceAll("");
247         result = trimPattern.matcher(result).replaceAll("
");
248
// 恢复替换掉的字符串
249         result = result.replaceAll(tempSingleCommentBlock, "//").replaceAll(tempMulitCommentBlock1, "/*")
250                 .replaceAll(tempMulitCommentBlock2, "*/");
251
252
return result;
253     }
254 }
255

使用注意事项


      使用了上面方法后,再运行程序,是不是发现每个页面查看源代码的时候都变成1行啦,还不错吧,但是在使用的时候还是要注意一些问题:

           1. 嵌入js本来想调用yuicompressor来压缩,yuicompressor压缩JS前,会先编译js是否合法,因我们嵌入的js中可能很多会用到一些服务器端代码,比如 var now = <%=DateTime.now %> ,这样的代码会编译不通过,所以无法使用yuicompressor。

              最后只能自己写压缩JS代码,自己写的比较粗燥,所以有个问题还解决,就是如果开发人员在一句js代码后面没有加分号的话,压缩成1行就很有可能出问题。所以使用这个需要保证每条语句结束后都必须带分号。


           2. 因为是在程序启动的时候压缩所有jsp(aspx),所以如果是用户请求的时候动态产生的html就无法压缩。

您需要登录后才可以回帖 登录 | 申请新用户

本版积分规则

小黑屋|手机版|Archiver|守望轩 ( 湘ICP备17013730号-2 )|网站地图

GMT+8, 2018-12-14 07:23 , Processed in 0.074993 second(s), 17 queries .

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表