首页 > 代码库 > 柯南君:教你如何对待大型网站平台的性能优化? 之 ”五“---常见的系统瓶颈及调优策略--web 前端性能优化策略(长篇总结)

柯南君:教你如何对待大型网站平台的性能优化? 之 ”五“---常见的系统瓶颈及调优策略--web 前端性能优化策略(长篇总结)

柯南君:教你如何对待大型网站平台的性能优化? 之 ”五“---常见的系统瓶颈及调优策略--web 前端性能优化策略(长篇总结)

        从企业架构上(见 http://blog.csdn.net/sun305355024sun/article/details/40920051)来说,企业架构可以分为两大部分:业务架构和IT架构,大部分企业架构方法都是从IT架构发展而来的,详细如下:
  • ① 业务架构:是把企业的业务战略转化为日常运作的渠道业务战略决定业务架构,它包括业务的运营模式流程体系组织结构地域分布等内容。
  • ② IT架构:指导IT投资设计决策的IT框架,是建立企业信息系统的综合蓝图,包括数据架构应用架构技术架构三部分。
  • 从企业IT架构体系上来看,特别是对于Web2.0网站来说,必须考虑的就是可扩展性:随着使用人数的增多,能够及时的扩展IT系统的能力。解决这个问题,通常有两种解决方式:Scale up和Scale out,两种扩容的方式 分别从两个维度来解决数据库压力。 这些不是本篇文章考虑重点;
  •  本篇更多讲的是IT架构的性能调优-《web前端性能调优策略-内容篇》

  一般来说,性能优化也就是下面的几个策略:

  • 用空间换时间。各种 cache 如 CPU L1/L2/RAM 到硬盘,都是用空间来换时间的策略。这样策略基本上是把计算的过程一步一步的保存或缓存下来,这样就不用每次用的时候都要再计算一遍,比如数据缓冲,CDN,等。这样的策略还表现为冗余数据,比如数据镜象,负载均衡什么的。
  • 用时间换空间。有时候,少量的空间可能性能会更好,比如网络传输,如果有一些压缩数据的算法,这样的算法其实很耗时,但是因为瓶颈在网络传输,所以用时间来换空间反而能省时间。
  • 简化代码。最高效的程序就是不执行任何代码的程序,所以,代码越少性能就越高。关于代码级优化的技术大学里的教科书有很多示例了。如:减少循环的层数,减少递归,在循环中少声明变量,少做分配和释放内存的操作,尽量把循环体内的表达式抽到循环外,条件表达的中的多个条件判断的次序,尽量在程序启动时把一些东西准备好,注意函数调用的开销(栈上开销),注意面向对象语言中临时对象的开销,小心使用异常(不要用异常来检查一些可接受可忽略并经常发生的错误),…… 等等,等等,这连东西需要我们非常了解编程语言和常用的库。
  • 并行处理。如果 CPU 只有一个核,你要玩多进程,多线程,对于计算密集型的软件会反而更慢(因为操作系统调度和切换开销很大),CPU 的核多了才能真正体现出多进程多线程的优势。并行处理需要我们的程序有 Scalability,不能水平或垂直扩展的程序无法进行并行处理。从架构上来说,这表再为——是否可以做到不改代码只是加加机器就可以完成性能提升?

  总之,根据2:8原则来说,20% 的代码耗了你 80% 的性能,找到那 20% 的代码,你就可以优化那 80% 的性能。 下面的一些东西都是我的一些经验,我只例举了一些最有价值的性能调优的的方法,供你参考,也欢迎补充。

  • (一)web 前端性能优化策略

1.web 前端优化最佳实践之 内容篇(雅虎团队经验:网站页面性能优化的 34条黄金守则 )

1) 尽量减少HTTP请求(Make Fewer HTTP Requests)

HTTP 请求:作为第一条,可能也是最重要的一条。根据数据分析,有很大一部分用户访问会因为这一条而取得最大受益。有几种常见的方法能切实减少 HTTP 请求:

① 合并文件 比如把多个CSS文件合并成一个;
     备注:可以参见 
②  CSS Sprites 利用CSS background 相关元素进行背景图绝对定位;
     备注:可以参见 CSS Sprites简介以及优缺点
③  图像地图 参见CSS Sprites:Image Slicing‘s Kiss of Death;
     备注:可以参见 CSS Sprites图片地图
④  内联图像 使用data:URL scheme 在实际的页面嵌入图像数据;
     备注:可以参见 内联地图

2)减少 DNS 查找 (Reduce DNS Lookups)

     
      ① 了解 DNS,以及如何查找DNS?

	        在浏览器中打开一个网站对于大多数用户来说是很简单的事情,输入网址,回车,网站就打开了。但是对于网站开发者来说,就在这短短一秒不到的时间里,发生了很多事情,DNS查找,建立连接,服务器处 理,              下载结果,客户端渲染等等。在这里,我们着重对DNS查找讨论一下。简单来说DNS查找就是将域名翻译成具体IP地址的过程,因为IP地址才是一台电脑在互联网上的唯一地址,域名只是用来方便人们记忆网站地            址的名称而已,我们不会通过身份证号去记住一位朋友,而是他的名字。DNS是通过一个分布式数据库系统维护的,系统下有很多节点,每个节点就是域名服务器,顶层是根域名服务器。对于DNS查找的原理在这里		     只是大概说一下;
      查找DNS是需要花费时间的,经验的总结是至少需要20毫秒左右的时间。在此期间,浏览器是无法下载其他任何内容资源的。所以浏览器会想办法对DNS查找的结果进行缓存。而除了浏览器的缓存之外,操作系统(             如windows)也会对DNS进行缓存,只不过浏览器太过于频繁,目前的主流浏览器都是使用自己的独特的缓存,而不使用操作系统的缓存
           ②  如何减少查找DNS
        减少来自不同域请求数量的方法有很多,比如,将css和javascript文件合并,在图片处理时尽可能使用css sprites(参见上边“ 尽量减少HTTP请求”)技术,将小图片合并到一张大图,尽量将外部域的对象下      载到本地服务器上等等。总之原则就是,页面打开时DNS查找次数越低,页面打开速度越快。
	A)  IE 中默认情况下对DNS的缓存时间为 30分钟。关于如何配置,可以通过阅读这篇文章了解更多信息。
	B)  Firefox默认的DNS缓存时间据说为1分钟,如果不满意这个选项,直接修改 network.dnsCacheExpiration 即可。 
	C)  Google Chrome默认的DNS缓存时间,据我观察也是1分钟,可以通过chrome://net-internals/#dns 这个地址查看。 
       那么,讲了这么多,了解这个只是对于我们网站设计和优化有何启示呢?
	A) 由于DNS查找是需要时间的,而且它们通常都是只缓存一定的时间,所以应该尽可能地减少DNS查找的次数。
	B) 减少DNS查找次数,最理想的方法就是将所有的内容资源都放在同一个域(Domain)下面,这样访问整个网站就只需要进行一次DNS查找,这样可以提高性能。
	C) 但理想总归是理想,上面的理想做法会带来另外一个问题,就是由于这些资源都在同一个域,而HTTP /1.1 中推荐客户端针对每个域只有一定数量的并行度(它的建议是2),那么就会出现下载资源时的排队现象           ,这样就会降低性能。
	D) 所以,折衷的做法是:建议在一个网站里面使用至少2个域,但不多于4个域来提供资源。我认为这条建议是很合理的,也值得我们在项目实践中去应用。

	虽然大多数浏览器都可以缓存域名,即把域名和IP对应存储在本地,这样在打开网站时如果网址已经存在本地缓存中时直接从缓存中读取即可,不需要再进行远程DNS查询。但是,一个网站上往往会有成百上千的对       象,javascipt,css,图片,ajax请求等等,这些对象可能来自不同的域,就算下载这些对象的时间很快,但是因为数量多,累加起来就足以引起用户的注意了。因此减少打开网站时的请求数量就是提高网站性能的有      效办法。

 3)避免跳转

        跳转是使用301和302代码实现的。下面是一个响应代码为301的HTTP头:
HTTP/1.1 301 Moved Permanently
Location: http://www.uran.cn
Content-Type: text/html
        ①  浏览器会把用户指向到Location中指定的URL。头文件中的所有信息在一次跳转中都是必需的,内容部分可以为空。不管他们的名称,301和302 响应都不会被缓存除非增加一个额外的头选项,
如Expires或者Cache-Control来指定它缓存。
        ②  <meta/>元素的刷新标签和JavaScript也可以实现URL的跳转,但是如果你必须要跳转的时候,最好的方法就是使用标准的3XXHTTP状态代码,这主要是为了确保“后退”按钮可以正确地使用
但是要记住跳转会降低用户体验。在用户和HTML文档中间增加一个跳转,会拖延页面中所有元素的显示,因为在HTML文件被加载前任何文件(图像、 Flash等)都不会被下载。
有一种经常被网页开发者忽略却往往十分浪费响应时间的跳转现象。这种现象发生在当URL本该有斜杠(/)却被忽略掉时。例如,当我们要访问http://dongpeng.juran.cn /apple时,实际上返回的是一个包含301代码的跳转,它指向的是http://dongpeng.juran.cn /apple/(注意末尾的斜杠)。在Apache服务器中可以使用Alias 或者 mod_rewrite或者the DirectorySlash来避免。

        ③ 连接新网站和旧网站是跳转功能经常被用到的另一种情况。这种情况下往往要连接网站的不同内容然后根据用户的不同类型(如浏览器类型、用户账号所属类型)来进行跳转。使用跳转来实现两个网站的切换十分简单,需要的代码量也不多。尽管使用这种方法对于开发者来说可以降低复杂程度,但是它同样降低用户体验。一个可替代方法就是如果两者在同一台服务器上时使用Alias和mod_rewrite和实现。如果是因为域名的不同而采用跳转,那么可以通过使用 Alias或者mod_rewirte建立CNAME(保存一个域名和另外一个域名之间关系的DNS记录)来替代。 

 4)可缓存的AJAX

 Ajax经常被提及 的一个好处就是由于其从后台服务器传输信息的异步性而为用户带来的反馈的即时性。但是,使用Ajax并不能保证用户不会在等待异步的 JavaScript和XML响应上花费时间。在很多应用中,用户是否需要等待响应取决于Ajax如何来使用。例如,在一个基于Web的Email客户端中,用户必须等待Ajax返回符合他们条件的邮件查询结果。记住一点,“异步”并不异味着“即时”,这很重要。
为了提高性能,优化Ajax响应是很重要的。提高Ajxa性能的措施中最重要的方法就是使响应具有可缓存性,具体的讨论可以查看Add an Expires or a Cache-Control Header。其它的几条规则也同样适用于Ajax:
① Gizp压缩文件
② 减少DNS查找次数
③ 精简JavaScript
④ 避免跳转
⑤ 
配置ETags
让我们来看一个例子:一个Web2.0的Email客户端会使用Ajax来自动完成对用户地址薄的下载。如果用户在上次使用过Email web应用程序后没有对地址薄作任何的修改,而且Ajax响应通过Expire或者Cacke-Control头来实现缓存,那么就可以直接从上一次的缓存中读取地址薄了。必须告知浏览器是使用缓存中的地址薄还是发送一个新的请求。这可以通过为读取地址薄的Ajax URL增加一个含有上次编辑时间的时间戳来实现,例如,&t=11900241612等。如果地址薄在上次下载后没有被编辑过,时间戳就不变,则从浏览器的缓存中加载从而减少了一次HTTP请求过程。如果用户修改过地址薄,时间戳就会用来确定新的URL和缓存响应并不匹配,浏览器就会重要请求更新地址薄。
   即使你的Ajxa响应是动态生成的,哪怕它只适用于一个用户,那么它也应该被缓存起来。这样做可以使你的Web2.0应用程序更加快捷。

 5)LazyLoad 推迟加载内容(按需加载内容)

① 页面加载过程,都需要加载什么?

	页面加载过程中,除了页面本身的内容外,可能需要加载很多额外的资源,例如 我们常说的:
		A)   脚本
		B)   样式表
		C)  图片
	  这一原则的核心是:延迟加载或者按需加载

② 你可以仔细看一下你的网页,问问自己“哪些内容是页面呈现时所必需首先加载的?哪些内容和结构可以稍后再加载?
把整个过程按照onload事件分隔成两部分,JavaScript是一个理想的选择。例如,如果你有用于实现拖放和动画的JavaScript,那么它就以等待稍后加载,因为页面上的拖放元素是在初始化呈现之后才发生的。其它的例如隐藏部分的内容(用户操作之后才显现的内容)和处于折叠部分的图像也可以推迟加载

③ 常用到的延迟加载方式

A)针对脚本的加载

我们可以想象一下,一个真正的网站项目中,会有各种各样的脚本文件,其中还包含很多基础的框架(例如jquery,knockoutjs 等),这些脚本文件可能都或多或少需要在页面中引用。问题在于,如果页面一多起来,或者复杂起来,我们可能不太能准确地知道某个页面是否真的需要某个脚本。(难道不是这样吗?),一个蹩脚的解决方案是,那么就在母版页中,一次性将所有可能用到的框架脚本都引用进来吧。你是这样做的吗?

如果这样做的话,对于小的项目来说,没什么问题,但是对大项目尤其是产品来说,尤其是,小步快跑,不断迭代的产品来说,简直是毁灭性的,后期将会有大量的,瘦身工作,否则,会影响整个产品的性能(柯南君深有体会)!

【案例分析】 

a)当一个网站有很多js代码要加载,js代码放置的位置在一定程度上将会影像网页的加载速度,为了让我们的网页加载速度更快,本文总结了一下几个注意点: 
1、延迟加载js代码 

备注:

① JS延迟加载方案,一般情况下都是用setTimeout来实现,这样通过延迟加载js代码,给网页加载留出更多的时间

<span style="white-space:pre">			</span><script language="JavaScript" src=http://www.mamicode.com/"" id="my"></script> >

② JS最后加载方案,在需要插入JS的地方插入以下代码: 

<span style="white-space:pre">			</span><SPAN id=L4EVER>LOADING...</SPAN> 
<span style="white-space:pre">		</span>     <span style="white-space:pre">	</span>当然,那个LOADING…你可以换成自己喜欢的小图片。看起来很有AJAX效果呢。 然后在页面最底端插入: 
<span style="white-space:pre">			<SPAN class=spanclass id=AD_L4EVER><script src=http://www.mamicode.com/"1.js"></script> 
<span style="white-space:pre">			<script>L4EVER.innerHTML=AD_L4EVER.innerHTML;AD_L4EVER.innerHTML="";</script> </span>

让JS最后加载方案二 ,这个牵涉到网页的加载顺序问题,例如引入外部js脚本文件时,如果放入html的head中,则页面加载前该js脚本就会被加载入页面,而放入body中,则会按照页面从上倒下的加载顺序来运行javascript的代码~~~ 所以我们可以把js外部引入的文件放到页面底部,来让js最后引入,从而加快页面加载速度。

B)针对图片的加载

   原理是这样:页面可见区域以下的图片先不加载,等到用户向下滚动到图片位置时,再进行加载。这样做的好处在哪里?——当页面有好几屏内容时,有可能用户只看前几屏的内容,这样我们就可以只加载用户需要看的图片,减少服务器向用户浏览器发送图片文件所产生的负荷。于是我打开了土豆网,没发现它的这种功能。但是一想,这确实是挺有意思的。这跟人人网的分批加载新鲜事的有异曲同工之妙。

【案例分析】 

a) Lazyload 延迟加载效果 与 ImagesLazyload 图片延迟加载效果

备注:

① Lazyload是通过延迟加载来实现按需加载,达到节省资源,加快浏览速度的目的。

②这个Lazyload主要特点是:支持使用window(窗口)或元素作为容器对象;

③对静态(位置大小不变)元素做了大量的优化;

④支持垂直、水平或同时两个方向的延迟。

请参见 延迟加载效果展示 之 Lazyload 延迟加载效果

ImagesLazyload 图片延迟加载效果

                                            在这里要感谢 cloudgamer 的贡献

b)以下是简单的HTML和JS——在IE下简单测试过了。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>lazyImage2.html</title>
	
    <meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
    <meta http-equiv="description" content="this is my page">
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    
    <!--<link rel="stylesheet" type="text/css" href=http://www.mamicode.com/"./styles.css" mce_href="styles.css">-->>C)针对CSS样式文件的加载

  为了防止Javascript脚本block住浏览器进程. 往往我们需要等整个Page加载后再加载Javascript脚本.可以使用LazyLoad library.在经过最小化压缩后只有966字节.LazyLoad将从你指定URL文件数组自动并行加载并且确保执行顺序.  例如常规加载2个JS文件,1个CSS文件:

<span style="font-size:10px;"> <link rel="stylesheet" href=http://www.mamicode.com/"http://www.asp.net/ajaxlibrary/Themes/Ajax/AspNetSiteStyles.css" type="text/css" />>使用LazyLoad加载多个JS文件以及CSS文件,看下面代码片断:

          
LazyLoad.js(['http://code.jquery.com/jquery-1.6.4.min.js', 'http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/jquery-ui.min.js'], function () {
              alert('jquery-1.6.4.min.js and jquery-ui.min.js have been loaded');    
               $("img").slideUp(1000);
             });
        
        // Load a CSS file and pass an argument to the callback function.
        LazyLoad.css('http://www.asp.net/ajaxlibrary/Themes/Ajax/AspNetSiteStyles.css', function (arg) {
             alert(arg);
         }, 'AspNetSiteStyles.css has been loaded');
在Firebug中加载顺序及时间轴如下图:


再看动态生成的HTML:


我们DEMO示例中BODY标签中的HTML是这样的,试图在动态加载JQuery库后对IMG标签做一个动画操作.

          <h2>
             Author: Petter Liu   
             <a href=http://www.mamicode.com/"http://www.cnblogs.com/wintersun" target="_blank">http://www.cnblogs.com/wintersun>很简单,用纯Javascript来实现核心的方法动态加载是类似这样的:

<span style="font-size:10px;"> window.onload = downloadComponents;
         // Download external components dynamically using JavaScript.
         function downloadComponents() {
             downloadJS("http://code.jquery.com/jquery-1.6.4.min.js");
             downloadCSS("http://www.asp.net/ajaxlibrary/Themes/Ajax/AspNetSiteStyles.css");
         }
         // Download a script dynamically.
         function downloadJS(url) {
            var elem = document.createElement("script");
            elem.src = http://www.mamicode.com/url;>


6)预加载

预加载和后加载看起来似乎恰恰相反,但实际上预加载是为了实现另外一种目标。预加载是在浏览器空闲时请求将来可能会用到的页面内容(如图像、样式表和脚本)。使用这种方法,当用户要访问下一个页面时,页面中的内容大部分已经加载到缓存中了,因此可以大大改善访问速度。
下面提供了几种预加载方法:
① 无条件加载:触发onload事件时,直接加载额外的页面内容。以Google.com为例,你可以看一下它的spirit image图像是怎样在onload中加载的。这个spirit image图像在google.com主页中是不需要的,但是却可以在搜索结果页面中用到它。
② 有条件加载:根据用户的操作来有根据地判断用户下面可能去往的页面并相应的预加载页面内容。在search.yahoo.com中你可以看到如何在你输入内容时加载额外的页面内容。
③ 有预期的加载:载入重新设计过的页面时使用预加载。这种情况经常出现在页面经过重新设计后用户抱怨“新的页面看起来很酷,但是却比以前慢”。问题可能出在用户对于你的旧站点建立了完整的缓存,而对于新站点却没有任何缓存内容。因此你可以在访问新站之前就加载一部内容来避免这种结果的出现。在你的旧站中利用浏览器的空余时间加载新站中用到的图像的和脚本来提高访问速度。

下面讲述预加载几种常用场景:

① 图片预加载

图片预加载是web开发中一种应用相当广泛的技术,比如我们在做图片翻转显示等特效的时候,为了让图片在转换的时候不出现等待,我们最好是先让图片下载到本地,然后在继续执行后续的操作。今天我们将来实现一个完整的图片预加载和处理图片加载后执行后续操作的代码。

下面的函数实现了一个我们想要的最基本的图片预加载效果

<span style="font-size:10px;">function preloadimages(arr){
    var newimages=[]
    var arr=(typeof arr!="object")? [arr] : arr  //确保参数总是数组
    for (var i=0; i<arr.length; i++){
        newimages[i]=new Image()
        newimages[i].src=http://www.mamicode.com/arr[i]>我们可以通过如下的方式加载我们想要的图片

preloadimages(['1.gif', '2.gif', '3.gif'])
上面的方法已经可以满足我们最基本的预加载图片的效果了,但情况往往并不如此,我们往往需要确切的知道图像是否被真正加载完成,并可能在后续执行一系列对图片的操作功能。幸运的是,这个功能实现起来并不难,我们可以使用onload和onerror事件去处理决定图片是否加载完成(或者失败)。在本文的最终实现代码中,我们将会把proloadimages()函数改造成如下的样子。
preloadimages(['1.gif', '2.gif', '3.gif']).done(function(images){
 //当图片全部加载完成之后,执行此处的代码
 //images参数是Array类型,对应加载进来的图像
 //images[0] 对应的是第一张图像
})
首先我们用image对象的onload和onerror事件处理函数来检测图片的加载情况(成功或失败),改造后的代码如下。

function preloadimages(arr){
    var newimages=[], loadedimages=0
    var arr=(typeof arr!="object")? [arr] : arr
    function imageloadpost(){
        loadedimages++
        if (loadedimages==arr.length){
            alert("图片已经加载完成")
        }
    }
    for (var i=0; i<arr.length; i++){
        newimages[i]=new Image()
        newimages[i].src=http://www.mamicode.com/arr[i]>

我们可以使用代码2的调用方法测试该函数,当图片全部加载完成(成功或失败)后,浏览器将会弹出“图片已经加载完成”的消息。

现在,我们将为preloadimages()函数增加一个回调函数来处理后续的操作

通常我们会为我们的preloadimages()函数增加一个匿名函数做为参数,来完成我们需要的功能。如此之后,我们调用preloadimages()函数的代码可能会如下面这样。

preloadimages(imagesarray, function(){
 //图片加载完成之后执行的操作
})

但是我们现在来做一点点改变,让代码看起来更直观,更易于理解,改造完成之后,preloadimages()函数的调用看起来如下所示。

preloadimages(imagesarray).done(function(){
 //图片加载完成后的操作
})
上面这种写法大家一看一定都会觉得非常清晰明了,那么接下来,我们继续来改造我们的preloadimages()函数。

<span style="font-size:10px;">function preloadimages(arr){   
    var newimages=[], loadedimages=0
    var postaction=function(){}  //此处增加了一个postaction函数
    var arr=(typeof arr!="object")? [arr] : arr
    function imageloadpost(){
        loadedimages++
        if (loadedimages==arr.length){
            postaction(newimages) //加载完成用我们调用postaction函数并将newimages数组做为参数传递进去
        }
    }
    for (var i=0; i<arr.length; i++){
        newimages[i]=new Image()
        newimages[i].src=http://www.mamicode.com/arr[i]>

上面的代码,我们稍作修改了几个地方: 

首先,我们增加了一个postaction函数,该函数被用来做为图片加载完成后的回调函数,用户可以在后面调用的时候用自己的处理函数覆盖掉该函数。

第二,我们的preloadimages()函数返回了一个空对象,其中包含一个简单的done()方法,这是实现本次改造的关键所在,确保了链式调用的实现。

最后,我们的调用变为如下形式

<span style="font-size:10px;">preloadimages(['1.gif', '2.gif', '3.gif']).done(function(images){
   alert(images.length) //alerts 3
  alert(images[0].src+" "+images[0].width) //alerts '1.gif 220'
})</span>
当然,我们还可以在done()里实现各种我们需要的图片操作!
② JS预加载

JavaScript事件加载本身并不是什么复杂的功能,通常来说,window.onload就够用了。如果想加载多个事件,我们可以采取以下方式:

    1. window.onload = function(){  
    2. func1();  
    3. func2();  
    4. func3();  
    5. //更多加载事件………………  
    6. }  

    但如果由于某种特殊需要,我们不能合在一起写吗?如当前区域是面向管理员,后台生成页面时只有当用户是管理员,页面才生成这部分,而这部分也用到一些特殊的脚本,上面的方法就歇菜了!

    JavaScript事件加载:前台与后台的尴尬

    1. //后台代码  
    2. < script type="text/javascript">  
    3. window.onload = function(){  
    4. func1();  
    5. func2();  
    6. //加载普通用户用到的脚本……  
    7. }  
    8. < /script>  
    9. < %# 以下脚本是为管理员准备的 %>  
    10. < % if @user.role == "manager"   %>  
    11. window.onload = function(){  
    12. func1();  
    13. func2();  
    14. //加载机密脚本……  
    15. }  
    16. < % end %>  

    这种情况生成出来的页面拥有两个window.onload代码块,很显然,第二个覆盖掉第一个。这时,轮到loadEvent函数出场了。

    1. var loadEvent = function(fn) {  
    2. var oldonload = window.onload;  
    3. if (typeof window.onload != ‘function‘) {  
    4. window.onload = fn;  
    5. }else {  
    6. window.onload = function() {  
    7. oldonload();  
    8. fn();  
    9. }  
    10. }  
    11. }  

    它非常完美地解决了互相覆盖的问题,用法如下:

    1. loadEvent(func1);  
    2. loadEvent(func2);  
    3. loadEvent(func3);  
    4. //更多加载事件  

    但现实的问题总是如此出奇不意,也如此刁钻邪门。最近我想把所有的函数放到一个闭包中,以免除命名冲突之苦,比如那个有名的$的DOM选择器。JQuery,Prototype,mootool都用它做选择器的名字,共存成了个严重的问题。

    1. (function(){  
    2. if(!window.JS){  
    3. window[‘JS‘] = {}  
    4. }  
    5. var onReady = function(fn){  
    6. var oldonload = window.onload;  
    7. if (typeof window.onload != ‘function‘) {  
    8. window.onload = fn;  
    9. }else {  
    10. window.onload = function() {  
    11. oldonload();  
    12. fn();  
    13. }  
    14. }  
    15. }  
    16. JS.onReady = onReady;  
    17. var $ = function(id){  
    18. return document.getElementById(id);  
    19. }  
    20. JS.$ = $;  
    21. })()  
    22.  

    报错,说找不到节点。为什么找不到呢?因为我们在调用它的时候,DOM树还没有建立起来!不会吧,网页是如此完整地显示我们眼前了。这与闭包的运作有关,当JS引擎解析到闭包的最后一个“}”,就把里面的东西锁到一个密封的环境中,JS继续向下执行,就修正不了闭包中的window的属性了。window有一个出名的属性,叫做document,它引用着整个DOM树(当然还有其他操作)。DOM树是一个复杂的键值对,当JS不断向下解析时,就不断往document添加相应的节点。但当它塞进闭包后,里面的时间就静止了,因此它还停留在解析head的阶段,document是残缺的,想获取body中的节点,当然是返回null了。于是问题的关键是如何让闭包内的document重新继续向下解析。 方法有两个,一个是利用侦听器(addEventListener与attachEvent),一个是利用基于时间轴的setTimeout与setInterval。

    1. (function(){  
    2. if(!window.JS){  
    3. window[‘JS‘] = {}  
    4. }  
    5. var addEvent = function( obj, type, fn ) {  
    6. if (obj.addEventListener)  
    7. obj.addEventListener( type, fn, false );  
    8. else if (obj.attachEvent) {  
    9. obj["e"+type+fn] = fn;  
    10. obj.attachEvent( "on"+type, function() {  
    11. obj["e"+type+fn]();  
    12. } );  
    13. }  
    14. };  
    15. var onReady = function(loadEvent,waitForImages) {  
    16. if(waitForImages) {  
    17. return addEvent(window, ‘load‘, loadEvent);  
    18. }  
    19. }  
    20. JS.onReady = onReady;  
    21. var $ = function(id){  
    22. return document.getElementById(id);  
    23. }  
    24. JS.$ = $;  
    25. })()  
    26.  

    OK,没问题。上面的onReady函数有一个可选参数,判断图片是否加载完毕。我们知道JS引擎会在完成DOM树后才开始处理图片与音频等东西,但如果我们的页面严重依赖于脚本布局呢?!我们想尽快让页面呈现出大体形态,这就用到domReady了。我们在原基础上改进它。

    1. (function(){  
    2. if(!window.JS){  
    3. window[‘JS‘] = {}  
    4. }  
    5. var addEvent = function( obj, type, fn ) {  
    6. if (obj.addEventListener)  
    7. obj.addEventListener( type, fn, false );  
    8. else if (obj.attachEvent) {  
    9. obj["e"+type+fn] = fn;  
    10. obj.attachEvent( "on"+type, function() {  
    11. obj["e"+type+fn]();  
    12. } );  
    13. }  
    14. };  
    15. var onReady = function(loadEvent,waitForImages) {  
    16. if(waitForImages) {  
    17. return addEvent(window, ‘load‘, loadEvent);  
    18. }  
    19. var init = function() {  
    20. if (arguments.callee.done) return;  
    21. arguments.callee.done = true;  
    22. loadEvent.apply(document,arguments);  
    23. };  
    24. if(!+"/v1"){  
    25. (function(){  
    26. try {  
    27. document.documentElement.doScroll("left");  
    28. catch(e) {  
    29. setTimeout( arguments.callee, 0 );  
    30. return;  
    31. }  
    32. init();  
    33. })();  
    34. }else{  
    35. document.addEventListener( "DOMContentLoaded"function(){  
    36. document.removeEventListener( "DOMContentLoaded", arguments.callee, false );  
    37. init();  
    38. }, false );  
    39. }  
    40. return true;  
    41. }  
    42. JS.onReady = onReady;  
    43. var $ = function(id){  
    44. return document.getElementById(id);  
    45. }  
    46. JS.$ = $;  
    47. })()  

    dom标准浏览器用DOMContentLoaded,这是非常正现的W3C论DOM方法,与FF的DOMMouseScroll 不一样,基本上所有非IE内核的浏览器最新版都支持它了。IE下我们可以通过侦听document. documentElement. doScroll()来判断DOM树是否完成,原理是IE下只有当DOM树构建完成后才能doScroll。但它还不是尽善尽美,它在IE下无法判定iframe的内容是否加载完毕。我们继续改进它。

    1. (function(){  
    2. if(!window.JS){  
    3. window[‘JS‘] = {}  
    4. }  
    5. var addEvent = function( obj, type, fn ) {  
    6. if (obj.addEventListener)  
    7. obj.addEventListener( type, fn, false );  
    8. else if (obj.attachEvent) {  
    9. obj["e"+type+fn] = fn;  
    10. obj.attachEvent( "on"+type, function() {  
    11. obj["e"+type+fn]();  
    12. } );  
    13. }  
    14. };  
    15. var onReady = function(loadEvent,waitForImages) {  
    16. if(waitForImages) {  
    17. return addEvent(window, ‘load‘, loadEvent);  
    18. }  
    19. var init = function() {  
    20. if (arguments.callee.done) return;  
    21. arguments.callee.done = true;  
    22. loadEvent.apply(document,arguments);  
    23. };  
    24. if(!+"/v1"){  
    25. if(window.self == window.top){  
    26. (function(){  
    27. try {  
    28. document.documentElement.doScroll("left");  
    29. catch(e) {  
    30. setTimeout( arguments.callee, 0 );  
    31. return;  
    32. }  
    33. init();  
    34. })();  
    35. }else{  
    36. document.attachEvent("onreadystatechange"function(){  
    37. if ( document.readyState === "complete" ) {  
    38. document.detachEvent( "onreadystatechange", arguments.callee );  
    39. init();  
    40. }  
    41. });  
    42. }  
    43. }else{  
    44. document.addEventListener( "DOMContentLoaded"function(){  
    45. document.removeEventListener( "DOMContentLoaded", arguments.callee, false );  
    46. init();  
    47. }, false );  
    48. }  
    49. return true;  
    50. }  
    51. JS.onReady = onReady;  
    52. var $ = function(id){  
    53. return document.getElementById(id);  
    54. }  
    55. JS.$ = $;  
    56. })()  

    我们简直是在重新实现jquery的$(document).ready(function(){ })!它功能非常强悍,配合利用闭包做成的命名空间,基本刀枪不入。而且它就只污染一个全局变量“JS”,可以与YUI媲美了(笑)。不过对于一般应用来说,我们用不着做到如此面面俱到。假如我们不需要对图片进行处理,页面也没有iframe,我们可以搞下面这个微缩版出来。

    1. (function(){  
    2. if(!window.JS){  
    3. window[‘JS‘] = {}  
    4. }  
    5. var onReady = function(loadEvent) {  
    6. if(!+"/v1"){  
    7. (function(){  
    8. try {  
    9. document.documentElement.doScroll("left");  
    10. catch(e) {  
    11. setTimeout( arguments.callee, 0 );  
    12. return;  
    13. }  
    14. loadEvent();  
    15. })();  
    16. }else{  
    17. document.addEventListener( "DOMContentLoaded", loadEvent, false );  
    18. }  
    19. }  
    20. JS.onReady = onReady;  
    21. var $ = function(id){  
    22. return document.getElementById(id);  
    23. }  
    24. JS.$ = $;  
    25. })()  
    26.  
② CSS预加载

为了提高网站的加载速度,有一个很重要的手段就是在用户浏览过程中的上游网站做一个文件的预加载。预加载文件一般有两种常用的方式:xhr和动态插入节点的方式。动态插入节点是最为简单也最为广泛的一种异步加载方式(例如yui的Get模块),然后使用动态插入节点方法加载的文件都会在加载后立即执行,javascript的执行一方面会占用浏览器js执行进程,另一方面也可能改变页面结构,而css的执行更有可能让整个页面变化。xhr方式虽然不会执行脚本,但是由于同域的限制,且如今网站的静态文件都是部署在cdn服务器上,如何预加载css js文件也变有点玄妙了。

具体的方法是,ie中使用  new Image().src 去预加载文件,而其他浏览器使用动态插入的 <object> 标签来完成加载

部分代码如下

codeCode highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/--> 1 window.onload = function () {

    var i = 0,
        max = 0,
        o = null,

        // list of stuff to preload
        preload = [
            'http://tools.w3clubs.com/pagr2/<?php echo $id; ?>.sleep.expires.png',
            'http://tools.w3clubs.com/pagr2/<?php echo $id; ?>.sleep.expires.js',
            'http://tools.w3clubs.com/pagr2/<?php echo $id; ?>.sleep.expires.css'
        ],
        isIE = navigator.appName.indexOf('Microsoft') === 0;

    for (i = 0, max = preload.length; i < max; i += 1) {

        if (isIE) {
            new Image().src = http://www.mamicode.com/preload[i];>备注:

几点说明:

A). new Image().src 之所以不能在ff中使用是因为ff对图片实现了一套单独的缓存。 同时safari和chrome看起来也没有被缓存。

B). 动态插入的 object 标签需要插入到非 head部分,以触发加载。

C). ie7 ie8 也可以通过一些代码使用动态object加载文件(代码注释中有提到)。但是作者发现object 通常会消耗很大, so...

7)减少DOM元素数量

一个复杂的页面意味着需要下载更多数据,同时也意味着JavaScript遍历DOM的效率越慢。比如当你增加一个事件句柄时在500和5000个 DOM元素中循环效果肯定是不一样的。
大量的DOM元素的存在意味着页面中有可以不用移除内容只需要替换元素标签就可以精简的部分。你在页面布局中使用表格了吗?你有没有仅仅为了布局而引入更多的<div>元素呢?也许会存在一个适合或者在语意是更贴切的标签可以供你使用。
8)根据域名划分页面内容

把页面内容划分成若干部分可以使你最大限度地实现平行下载。由于DNS查找带来的影响你首先要确保你使用的域名数量在2个到4个之间。例如,你可以把用到的HTML内容和动态内容放在www.example.org上,而把页面各种组件(图片、脚本、CSS)分别存放在 statics1.example.org和statics.example.org上。

你可在Tenni Theurer和Patty Chi合写的文章Maximizing Parallel Downloads in the Carpool Lane找到更多相关信息。

9)使iframe的数量最小

    ifrmae元素可以在父文档中插入一个新的HTML文档。了解iframe的工作理然后才能更加有效地使用它,这一点很重要。
<iframe>优点:
① 解决加载缓慢的第三方内容如图标和广告等的加载问题 
② Security sandbox 
③ 并行加载脚本 
<iframe>的缺点:
① 即时内容为空,加载也需要时间 
② 会阻止页面加载 
③ 没有语意 

10)不要出现404错误

HTTP请求时间消耗是很大的,因此使用HTTP请求来获得一个没有用处的响应(例如404没有找到页面)是完全没有必要的,它只会降低用户体验而不会有一点好处。
有些站点把404错误响应页面改为“你是不是要找***”,这虽然改进了用户体验但是同样也会浪费服务器资源(如数据库等)。最糟糕的情况是指向外部 JavaScript的链接出现问题并返回404代码。首先,这种加载会破坏并行加载;其次浏览器会把试图在返回的404响应内容中找到可能有用的部分当作JavaScript代码来执行

11)使用内容分发网络

用户与你网站服务器的接近程度会影响响应时间的长短。把你的网站内容分散到多个、处于不同地域位置的服务器上可以加快下载速度。但是首先我们应该做些什么呢?
按地域布置网站内容的第一步并不是要尝试重新架构你的网站让他们在分发服务器上正常运行。根据应用的需求来改变网站结构,这可能会包括一些比较复杂的任务,如在服务器间同步Session状态和合并数据库更新等。要想缩短用户和内容服务器的距离,这些架构步骤可能是不可避免的。
要记住,在终端用户的响应时间中有80%到90%的响应时间用于下载图像、样式表、脚本、Flash等页面内容。这就是网站性能黄金守则。和重新设计你的应用程序架构这样比较困难的任务相比,首先来分布静态内容会更好一点。这不仅会缩短响应时间,而且对于内容分发网络来说它更容易实现。
内容分发网络(Content Delivery Network,CDN)是由一系列分散到各个不同地理位置上的Web服务器组成的,它提高了网站内容的传输速度。用于向用户传输内容的服务器主要是根据和用户在网络上的靠近程度来指定的。例如,拥有最少网络跳数(network hops)和响应速度最快的服务器会被选定。

一些大型的网络公司拥有自己的CDN,但是使用像Akamai Technologies,Mirror Image Internet, 或者Limelight Networks这样的CDN服务成本却非常高。对于刚刚起步的企业和个人网站来说,可能没有使用CDN的成本预算,但是随着目标用户群的不断扩大和更加全球化,CDN就是实现快速响应所必需的了。以Yahoo来说,他们转移到CDN上的网站程序静态内容节省了终端用户20%以上的响应时间。使用CDN是一个只需要相对简单地修改代码实现显著改善网站访问速度的方法。

12)为文件头指定Expires或Cache-Control

这条守则包括两方面的内容:
对于静态内容:设置文件头过期时间Expires的值为“Never expire”(永不过期)
对于动态内容:使用恰当的Cache-Control文件头来帮助浏览器进行有条件的请求
网页内容设计现在越来越丰富,这就意味着页面中要包含更多的脚本、样式表、图片和Flash。第一次访问你页面的用户就意味着进行多次的HTTP请求,但是通过使用Expires文件头就可以使这样内容具有缓存性。它避免了接下来的页面访问中不必要的HTTP请求。Expires文件头经常用于图像文件,但是应该在所有的内容都使用他,包括脚本、样式表和Flash等。
浏览器(和代理)使用缓存来减少HTTP请求的大小和次数以加快页面访问速度。Web服务器在HTTP响应中使用Expires文件头来告诉客户端内容需要缓存多长时间。下面这个例子是一个较长时间的Expires文件头,它告诉浏览器这个响应直到2010年4月15日才过期。
Expires: Thu, 15 Apr 2010 20:00:00 GMT
如果你使用的是Apache服务器,可以使用ExpiresDefault来设定相对当前日期的过期时间。下面这个例子是使用 ExpiresDefault来设定请求时间后10年过期的文件头:
ExpiresDefault "access plus 10 years" 
要切记,如果使用了Expires文件头,当页面内容改变时就必须改变内容的文件名。依Yahoo!来说我们经常使用这样的步骤:在内容的文件名中加上版本号,如yahoo_2.0.6.js。
使用Expires文件头只有会在用户已经访问过你的网站后才会起作用。当用户首次访问你的网站时这对减少HTTP请求次数来说是无效的,因为浏览器的缓存是空的。因此这种方法对于你网站性能的改进情况要依据他们“预缓存”存在时对你页面的点击频率(“预缓存”中已经包含了页面中的所有内容)。 Yahoo!建立了一套测量方法,我们发现所有的页面浏览量中有75~85%都有“预缓存”。通过使用Expires文件头,增加了缓存在浏览器中内容的数量,并且可以在用户接下来的请求中再次使用这些内容,这甚至都不需要通过用户发送一个字节的请求。

待续....

柯南君:教你如何对待大型网站平台的性能优化? 之 ”五“---常见的系统瓶颈及调优策略--web 前端性能优化策略(长篇总结)