网站再优化

今天闲来无事,就用Firebug查看首页的HTTP瀑布图,测试页面加载时间,发现在外联的css和js上时间开销还是挺大的,虽然css和js都进行了gzip压缩,但是解析延迟在某些时候还是比较厉害,不是css解析延迟就是js解析延迟,在读取缓存的时候,也有一定的读取的时间开销。

鉴于这种情况,那么干脆就将首页的css和js都嵌入到页面中吧,把css放到head、js放到body闭合标签之前,反正这两个文件在gzip压缩的条件下也总共才三四K而已,而子页面里还是使用外联css和js的方式。

经过这样倒弄,首页加载速度平均有100ms的提升,欣喜若狂。更好笑的是yslow、page speed测试都得了满分,哈哈……

让首页嵌入css和js,而子页面外联的方式,主要是出于这样的一个目的:用户第一次浏览到首页的时候,页面可以尽快的呈现,而到了子页面,就加载css和js,这样的用户体验就有了一个类似于“稳步退化”的感觉,使得用户不会在起先的时候就有个不良感觉,而之后的访问,子页面就可以充分的利用缓存,因为子页面可能包含图片、flash等等,缓解HTTP的并发数。

还有,之前在浏览器下可能会有短暂的无样式与渲染样式之前切换的闪白,目前也得到了比较好的缓解。

谈谈松散耦合、颗粒度

“松散耦合”、“颗粒度”是相对于javascript库或者javascript框架来说的,它涉及的概念和包含的知识是很多的,而其中之一,就是说javascript库或者框架从整理来看是容易拆分,也容易自定义的组合,来实现特定需求的功能。之前有很多说法说YUI的编写方法才更接近于javascript的本质,虽然上手的门槛很高,但是对于一个真正的javascript程序员来说,反而更值得;而jQuery似乎另类了,改变了普通的编写javascript的编写方式,虽然说它封装的很好,“用最少的代码实现复杂的功能”,但是它的可读性就很差了,虽然门槛很低,毫无javascript基础的程序员在看了它的API文档之后都能够利用它写出很多丰富的效果。但是,作为程序员,这样下去行吗?

jQuery是毒药 —— 很多人都这么说。本人也举得,从一个程序员的成长来看,jQuery是弊大于利。我们更应该做的是学习它的原理和架构,而不是拿来用而已。

一个很经常会发生的问题:我只需要用到一个javascript库中的几个方法,却需要加载整个库的体积,而整个库封装的又十分复杂,拆分起来十分繁琐,怎么办?自己写呗……

一个javascript库的本意虽然说是为了提高开发效率、解决浏览器的兼容性问题、统一开发规范以及解决安全隐患等等。但是在设计javascript库的时候,我们是不是应该多留多点空间给使用者去DIY它的具体的功能,让使用者多点接触到javascript的本质,以及非常容易的缩减库的体积呢?就从这几点出发,YUI3是比较符合“松散耦合”、“颗粒度”的原则的,虽然说jQuery可以通过fn方法来扩展,但都是基于它整个已经封装好的库来说的。但是YUI3也有它的缺点:学习成本高、整体体积庞大、可能一步小心在use方法里多加载几个文件,或者使用”*”,那么需要异步加载的js文件也是很多的、以及它内部组件之间的依赖关系也是比较复杂的。

为了说明我的想法,首先从我编写的query鸡查询函数(目前还没完善,先拿来做例子)开始说起,我的想法是:只在query里实现最基本的selector查询,比如ID、class、tag、属性,之后用户可以自定义selector添加到查询函数当中,来实现自己特定的查询需求,从这点出发,改写了之前所写的query代码(之前的query.js),目前它只集合了ID、class、tag、属性选择器(或者也可以干脆连这些使用者都可以重新定义自己的功能函数,自己来实现,DIY很爽~),其他的自定义选择器都可以自己DIY,并且编写它的实现方式(留给使用者更多的编写底层代码的空间,爱怎么实现就怎么实现,方法是多样的),比如:给selector添加“:first”和“:l”来实现CSS3中的:first-child和:last-child的功能:

//q:表示当前的selector,比如“div p:first”,就是div和p:first,使用split通过空格切分的单个selector
//p:表示是上一级查询的结果,是数组类型的。
query.config.addSelector(":first",function(q,p){
  var tagName = q.split(":first")[0],
	  returnEl = [],
	  index=0,
	  tmp=null;
  for(var i=0,l=p.length;i<l;i++){
	(tmp=query(tagName,[p[i]])) && (returnEl[index++]=tmp[0]);
  }
  return returnEl;
});
///////////////////////////////////////////////////////////
query.config.addSelector(":l",function(q,p){
  var tagName = q.split(":l")[0],
	  returnEl = [],
	  index=0,
	  tmp=null;
  for(var i=0,l=p.length;i<l;i++){
	(tmp=query(tagName,[p[i]])) && (returnEl[index++]=tmp[tmp.length-1]);
  }
  return returnEl;
});

通过这样的方式,使得core核心代码尽可能的小,可扩展性强,同时也提供给使用者更多的编码空间,了解javascript这门语言的核心知识。目前经过压缩过后的query2-min.js和未压缩的query2.js提供浏览。并附带实例:《点点看呗

扯谈完毕,欢迎拍砖~

在IE中模拟Worker

2010-8-8 update:在IE8下的eval是不支持下面两种的用法的,很杯具,都会提示“对象不支持此属性或方法”的错误。

//第一:
eval("onmessage=function(str){alert(str);}");
onmessage("shllo");
//第二:
var s = document.createElement("script"),h=document.getElementsByTagName("head")[0];
s.text="onmessage=function(str){alert(str);}";
h.appendChild(s);

onmessage("hello");

说到这个,还发现一点很容易造成失误的地方:在浏览器解析过的script中的代码,浏览器不会重新对该script标签内的行内脚本、外联脚本执行,就算是重新给script标签定义行内脚本或者修改它的src来链接到其他的javascript脚本,浏览器都不会重新解析。

——————- 提IE8 eval的兼容性分界线 ——————–

从上一篇文章《Web Worker浅析》中,我们了解到Worker的思维是跟Ajax类似的,包括它也不支持跨域调用javascript文件,这说明底层的数据交互还是跟Ajax的模式类似(也有可能就是使用了Ajax的方式)。但是由于IE8及以下版本都不支持Worker,所以IE就不能充分利用Worker的优点来优化浏览器执行javascript代码的性能了。但是既然Worker跟Ajax类似,那么就让IE使用Ajax的方式来实现吧,这也不是不行的。

在IE中实现Worker机制,一个比较棘手的问题是postMessage、onmessage在主页面和Worker之间的调用问题,从代码上看,主页面是通过Worker类的实例化对象来调用postMessage、onmessage,而Worker里是直接声明和调用。所以在IE里,就可以模拟Worker的操作方式了。下面是我在IE下实现的方式:

  1. 在IE下重新定义Worker类,并且带有一个postMessage方法,这样就的话不会跟支持Worker的浏览器相冲突
  2. 使用Ajax的方式来加载外联的javascript文件,并通过eval执行返回的代码
  3. 通过一个全局的字面量对象,来实现两个文件之间的数据传输
  4. 不能改变标准的Worker类的编写方式,这个是一定要做到的

从上面的几点思路出发,编写了下面的实现代码:

(function(g){
   if(!document.all) return;

   var xhr=function(){
     var x = null;
     try{
	   x = new ActiveXObject("Msxml2.XMLHTTP");
	 }catch(e){
       x = new ActiveXObject("Microsoft.XMLHTTP");
	 }
	 return x;
   }

   g.postMessage = function(data){
     g._evt_.data = data;
   }

   var Worker = function(url){
     this.url = url;
   }

   Worker.prototype={
     postMessage:function(data){
       g._evt_={};
	   g._evt_.data=data;
	   var x=xhr(),t=this;
	   x.open("GET",this.url,true);
	   x.onreadystatechange=function(){
	     if(x.readyState === 4 && (x.status === 304 || x.status === 200)){
                   //直接使用eval(x.responseText)在IE8下会提示错误,很奇怪,可能跟eval函数有关了
                   //为此不得不使用这种不太牢固的方法
		   eval(x.responseText.replace("onmessage","var onmessage"));
		   onmessage(g._evt_); //执行Worker中定义的onmessage方法
		   t.onmessage(g._evt_); //执行主页面中的onmessage方法
		   g._evt_ = null;
		 }
	   }
	   x.send();
	 }
   }
   g.Worker = Worker;
})(this);

在使用上有一点得注意,虽然在代码上没改变,但是在主页面中postMessage和onmessage的顺序得保持:先写onmessage、接着写postMessage方法,主要是为了在IE下能兼容。比如:

var worker = new Worker("js.js");

worker.onmessage = function(evt){
  alert(evt.data);
}
 //注意postMessage方法一定要在onmessage后面声明,否则会导致代码只会有一次有效。
worker.postMessage("supersha");

到目前为止,测试还算良好,《测试页面》,在各个浏览器下都能够跑起来。

目前这个只是个简单的实现方案,代码上还是比较简单的,有待进一步的完善……

flush()和ob_flush();flush()组合的差别

2010-7-29 Updates:

如果将render解析为“呈现”,即是将元素显示到视窗中,那么Nicholas的说法在大部分浏览器下是正确的,经过下面使用PHP方式的测试,在没有将闭合标签加载下来之前,页面无法显示h1标签内容,但是IE除外,它仍然显示内容和应用样式。

同时还有一点需要说明的是:一些文章说在head和body之间插入flush()方法来尽早的输出head的内容,以至于尽快的下载样式资源,这也是有一定道理的,但是由于刷新输出的head内容是不可见的,所以页面上还是会一片空白,等待内容的呈现。所以比较合理的做法是将内容分块,或者将页面内容分为head、body、footer等俺顺序使用flush刷新输出,这样就使得有一个比较流畅的页面显示效果。

本人的blog正在这样实践着……

————————纠正分界线—————————

在Nicholas的一个PPT中看到了这句话“The browser won’t render a block-level element inside of <body>
until the closing tag has been received”,意思就是“在body中,浏览器在一个块级元素的闭合标签未加载下来之前不会渲染该元素”,第一次看到这样的说法,感到很奇妙,到底是不是真的呢?

首先,使用javasript来进行测试,在一个块级元素中嵌套一个脚本,来获取该块级元素的innerHTML,《测试用例》,测试结果上面的说法错误。

接着测块级元素的style,使用js输出块级元素的style属性值,《测试用例》,测试结果上面的说法错误。

接着使用PHP的方式,在块级元素的闭合标签前面加上<php flush(); sleep(3); ?>,来查看是否会刷新输出内容,测试结果就是文章标题中所说明的问题,使用flush()和ob_flush();flush()组合的结果是不同的,并且在不同的浏览器下行为又有所不同。《flush()》,《ob_flush();flush()》。

PHP的测试结果为:使用flush()的形式的时候,Opera、Chrome、Safari、IE并没有在flush执行的时候刷新输出前面的内容;使用ob_flush();flush()组合的时候,Opera、Chrome、Safari、IE可以刷新输出前面的内容,但是这里又有个小插曲,IE可以输出元素闭合标签之前的内容,并可以渲染样式,但是Opera、Chrome、Safari只输出该元素之前的内容,该元素内的文本内容不会刷新输出。或许大家没有看到我提及Firefox浏览器的情况,经过测试,Firefox杯具的是两种形式都不会刷新输出内容,需要等到整个HTML文档加载完成才显示页面。为此,应该避免下面的情况:

  <div>Hello baby,www.ilovejs.net is here...<?php ob_flush();flush(); ?></div>
  <!--或者-->
  <div>Hello baby,www.ilovejs.net is here...<?php flush(); ?></div>

经过这些测试,可以得到明显的结论:在输出刷新方面,尽量使用ob_flush();flush()组合,并且避免在HTML标签的闭合标签之前刷新输出。

浅析Memoization

Memoization,简单的说就是优化计算机的性能,缓存那些重复性的函数操作和计算,使得第一次之后的调用可以直接从缓存中得到结果,而无需重新计算和运行复杂、费时的函数。这个跟Lazy Definition的原理是比较相似的,Lazy Definition主要是对函数进行重复定义,避免浏览器检测等恶心的事情。在javascript里实现Memoization的技术,Keith Gaughan做了相关的叙述:Memoization in JavaScript。特别是它对Fibonacci的优化让我特别的玩味。

对于Fibonacci普通的实现方式是:

//这个性能不咋的,函数调用太频繁了
function Fib(n) {
    if (n < 2) { return n;}
    return Fib(n - 1) + Fib(n - 2);
}

Keith Gaughan对它的实现方式用Memoization进行了优化:

var IterMemoFib = function() {
    var cache = [1, 1];
    var fib = function(n) {
        if (n >= cache.length) {
            for (var i = cache.length; i <= n; i++) {
                //这句代码很耐人寻味,还有循环的方式
                cache[i] = cache[i - 2] + cache[i - 1];
            }
        }
        return cache[n];
    }
    return fib;
}();

上面关于Fibonacci的优化,就是使用了Memoization技巧,将上一步的加法操作缓存起来,用户循环中下一轮的递增。缓存计算结果是Memorize应用最多的需求,避免了对计算结果的重复性计算,但是如果计算结果是动态的呢,或者说你想获取动态修改过后的计算结果呢? 比如DOM的length属性,这个是易变的,这个时候就不能去获取缓存的结果了,需要重复计算。

(全文…)

用于测试页面Repaint的firebug扩展 — Repeat Test

归家了,博客都没怎么更新,感觉有些不适应了。这几天忙里偷闲,研究了下firebug扩展的开发原理,倒弄了两三天,firebug的架构也确实比较复杂,而且发现Firefox下的扩展在开发中编写js代码并没有想象中的容易,平时认为合理的js代码,在Firefox的插件里跑起来,却总是会出现诸多错误,很纠结。仔细查看了firebug的源码中的xul文件,觉得firebug的架构和方法调用方式,也确实值得借鉴,语意化十足:利用xul文件来规划布局,js实现功能,css实现样式布局,properties格式的文件来储存文字信息……

Repaint 跟踪浏览器的渲染》从这篇文章中了解到,原来Firefox还有一个独有的事件:MozAfterPaint供我们来测试页面Repaint的数据,这个事件的具体使用方式和参数设置可以查看前面给出链接中Mozilla的官方叙述。PJhome网站中给出的例子是需要通过使用Greasemonkey来自定义一个脚本,我感觉这有诸多不便:不便于操作;开始和结束等方面的不变……为此,我将这个功能集成到firebug下,基本上web开发人员都会安装firebug,所以倒是便利了许多。下面就来说说Repaint Test的具体使用方式:

  1. 首先用Firefox3.5以上的版本(因为MozAfterPaint事件是在3.5版本之后才受支持的)下载Repaint Test文件:Repaint Test,直接点击链接就可以安装了。
  2. 打开firebug主界面,其中就有一个叫“Repaint Test”的面板,点击它,就出现了它的操作按钮组:

    Repaint Test面板下面有“Self start record”、“Start”、“Stop and Show”、“Clear”按钮,同时面板右边还有下拉菜单,提供了“Configuration”、“Zoom(文字大小)”、“Help”等功能。顾名思义,Configuration是用于配置扩展内部的一些属性。
  3. “Self start record”按钮用于在页面加载后,在设置的时间内自动开始记录Repaint区域数据;“Start”按钮用于手动设置记录Repaint的开始;“Stop and Show”用于停止记录Repaint区域数据并显示Repaint区域(红色高亮显示),这里有定时器来显示每一个记录的Repaint数据。“Clear”按钮用于清空输出内容。
  4. 在这里需要明白的是:“Self start record”和“Stop and Show”按钮内部都设定了定时器,不过间隔时间可以自定义,在Configuration弹出框里可以设置。但是最后的展示输出Repaint区域数据都需要点击“Stop and Show”来完成。

  5. 还有,输出的每一项Repaint的区域数据,都可以点击,再现Repaint区域的高亮效果。

PJhome中,还介绍了IE9提供了更多开放的接口供前端开发人员进行性能测试:《IE9允许前端开发获取到页面性能数据》,Very perfect!!但是IE系列总会有很多让人抹汗的地方,IE9的这套接口能否满足以后页面性能测试的需要,还有待测试和考验。

最后,欢迎大家对这个Repaint Test扩展进行测试和提出更好的建议,尽力去完善它的功能和使它在使用上更加的简便……