无阻塞加载脚本[译]+示例

      随着越来越多的站点向”Web2.0″应用发展,脚本的数量也在迅速增加。而与此同时脚本给页面性能带来的负面影响也是令人担忧的。在主流浏览器(例如IE6、IE7)中,由于脚本而产生的阻塞有以下两种:

  • 脚本会阻塞下载位于它之后的资源。
  • 脚本会阻塞渲染位于它之后的元素。

 
Scripts Block Downloads example 示例演示了以上两点。在该示例的页面中包含两个外部脚本,紧随其后的是一个图片资源、一个样式表以及一个iframe。从在iE7中运行该示例时的HTTP瀑布图中可以看出,第一个脚本阻塞了所有资源(包括第二个脚本)的下载,然后是第二个脚本阻塞了其后所有资源的下载,再往后可以看出图片、样式表和iframe并行地完成了下载。当你仔细观察页面的渲染过程时,你会发现,位于脚本上方的文字段落在页面加载的一开始就完成了渲染。然而脚本之后的内容的渲染过程在脚本下载完之前一直处于阻塞的状态。

IE6&7, Firefox 2&3.0, Safari 3, Chrome 1, and Opera中的脚本阻塞

由于浏览器是单线程的,因此脚本在执行的时候会阻塞下载其他资源,这一点很容易理解。但即便仅仅是在下载脚本,浏览器也无法下载其他资源就比较令人费解了。这一现象在现代浏览中也存在,包括IE8、Safari4和Chrome2。在IE8中运行Scripts Block Downloads example 示例时的瀑布图显示脚本以及样式表确实已经可以并行下载了,但是其他的图片和iframe仍然会被阻塞。Safari4和Chrome2的情况也是类似的。并行下载有所改善,但仍然有待改进。

 

即便是在 IE8, Safari 4, 和 Chrome 2中,脚本仍然会阻塞资源下载

      幸运的是,目前有很多方式可以实现脚本的无阻塞加载,即便是在旧浏览器中也有效。不幸的是,这些方式都需要开发者付出额外的劳动。

      目前实现脚本的无阻塞加载主要有6种方式:

      XHR Eval – 通过XHR获取脚本并用Eval来执行responseText。

      XHR Injection – 通过XHR获取脚本,在页面中创建一个script元素并将它的text属性设置为responseText。

      Script in Iframe – 将需要的脚本放到一个页面中,然后通过iframe来加载该页面。

      Script DOM Element – 创建一个脚本元素并将它的src属性设置为脚本的URL。

      Script Defer – 给script标签添加defer属性。该方法原本仅适用于IE,但是目前对于Firefox3.1同样有效。

      document.write Script Tag – 通过document.write将<script src=””>写入页面。该方式仅在IE中能解决阻塞问题。

      以上六种技术存在很多不同点,在下面的表格中一一列出。其中有些技术不能用于跨站脚本,而有些技术可以让你在现有代码上做少量的修改即可满足无阻塞的需求。有一个大家不曾广泛讨论的不同点是对于浏览器忙碌状态的影响,包括浏览器的状态栏、进度条、Tab图标以及鼠标。当你加载多个彼此间有依赖关系的脚本时,还需要一种能够保证执行顺序的技术。

技术 

 

并行下载 

 

可以跨域 

 

存在Script标签 

 

忙碌指示 

顺序保证 

大小 (bytes)

XHR Eval

IE, FF, Saf, Chr, Op

no

no

Saf, Chr

~500

XHR Injection

IE, FF, Saf, Chr, Op

no

yes

Saf, Chr

~500

Script in Iframe

IE, FF, Saf, Chr, Op

no

no

IE, FF, Saf, Chr

~50

Script DOM Element

IE, FF, Saf, Chr, Op

yes

yes

FF, Saf, Chr

FF, Op

~200

Script Defer

IE, Saf4, Chr2, FF3.1

yes

yes

IE, FF, Saf, Chr, Op

IE, FF, Saf, Chr, Op

~50

document.write Script Tag

IE, Saf4, Chr2, Op

yes

yes

IE, FF, Saf, Chr, Op

IE, FF, Saf, Chr, Op

~100

现在的问题是:哪一种技术是最好的?这取决于你的应用场景:脚本是否和主页面同域;是否需要保证脚本的执行顺序;浏览器的忙碌指示是否应该被触发。

在大多数情况下,Script DOM Element是一个好的选择。这种方式适用于所有的浏览器,而且没有跨域的限制,实现起来也非常的简单和易于理解。唯一的缺点是不能保证各个脚本的执行顺序。如果需要加载多个有依赖关系的脚本,应该将这些脚本拼成一个来保证其按需要的顺序执行,或者使用别的技术。

原文地址:http://www.stevesouders.com/blog/2009/04/27/loading-scripts-without-blocking/

具体示例

1. 原始页面(无忧化,直接链入外部script): http://varnow.org/pages/load-scripts-without-blocking.html

时间瀑布图

在无忧化的页面中,第一个脚本的下载和执行阻塞了其后的脚本下载以及页面的渲染,导致整个页面渲染完成需要4.8s。

2. 使用XHR Eval加载脚本: http://varnow.org/pages/load-scripts-without-blocking-xhr-eval.html

代码:

Ajax.get('/resources/js/lib/jquery.js',function(xhr){
	eval(xhr.responseText);
});
Ajax.get('/resources/js/lib/ext-core-debug.js',function(xhr){
	eval(xhr.responseText);
});

 时间瀑布图

通过使用Ajax加载脚本来优化,页面整体的渲染时间和加载时间为3.5s左右,这主要依赖于解决了脚本加载阻塞的问题,从上图中可以看到两个脚本是并行加载的,而且未阻塞后面资源的加载。该方法还有一个优点在于不会触发IE和FF的资源加载状态条。
该方法的缺点在于:
(1)由于ajax请求无法跨域,因此加载的脚本必须与页面同域
(2)不适用于加载多个有依赖关系的脚本,因为该方法无法保证脚本加载的顺序。

 3. 使用XHR获取脚本并创建script标签: http://varnow.org/pages/load-scripts-without-blocking-xhr-inject.html

代码

Ajax.get('/resources/js/lib/jquery.js',function(xhr){
   injectScript(xhr.responseText);
  });
  Ajax.get('/resources/js/lib/ext-core-debug.js',function(xhr){
   injectScript(xhr.responseText);
  });
  function injectScript(scriptText){
   var s = document.createElement('script');
   s.text = scriptText;
   document.getElementsByTagName('head')[0].appendChild(s);
  }

时间瀑布图

特点以及效果同方法2 

4.  将需要加载的脚本放入一个单独的页面并用iframe加载: http://varnow.org/pages/load-scripts-without-blocking-iframe.html

时间瀑布图

从上图可以看到,iframe的加载是在父页面DOM解析完成后开始的,而且iframe中的脚本加载对父页面未造成阻塞。使用该方法的一个不好的方面是当加载未完成时,浏览器的状态条会一直显示为加载中;好处在于可以保证脚本加载的顺序是符合预期的,而且加载任何域下的脚本。

5. 通过Dom操作创建script标签并设置src来加载脚本: http://varnow.org/pages/load-scripts-without-blocking-dom-script.html

代码

  function importScript(scriptUrls){
   for( var i =0,l=scriptUrls.length; i < l; i++){
    var s = document.createElement('script');
    s.src = scriptUrls[i];
    document.getElementsByTagName('head')[0].appendChild(s);
   }
  }
  importShttp://varnow.org/files/cript(['/resources/js/lib/jquery.js','/resources/js/lib/ext-core-debug.js']);

时间瀑布图

效果与使用iframe的方法基本相同,不过该方式实现起来更为简单,而且兼备跨域的功能,因此是目前使用较多的以一种方式。该方式在IE下不会导致浏览器进度条出现加载中的状态,在FF、Safari和Chrome下均会触发状态条

6.  使用defer延迟加载: http://varnow.org/pages/load-scripts-without-blocking-defer.html

代码


时间瀑布图

在该例子中,仅仅对第二个脚本即ext-core-debug.js使用了defer,原因接下来说明。从图中可以看出,没有使用defer进行加载的prototype.js非常明显的阻塞了后续资源的加载,而使用了defer的ext-core-debug.js则没有阻塞效果。

该方法的优点是可以跨域、无需额外脚本,实现起来非常简单。但缺点似乎更多:仅适用于IE、而且对于脚本有要求。所谓脚本有要求是指在脚本中不能使用document.write,这会导致整个HTML页面被重置。这也正是不能给prototype.js使用defer的原因,下面是prototype.js中的一段代码:

function() {
  /* Support for the DOMContentLoaded event is based on work by Dan Webb,
     Matthias Miller, Dean Edwards and John Resig. */
var timer;
function fireContentLoadedEvent() {
    if (document.loaded) return;
    if (timer) window.clearInterval(timer);
    document.fire("dom:loaded");
    document.loaded = true;
  }
if (document.addEventListener) {
    if (Prototype.Browser.WebKit) {
      timer = window.setInterval(function() {
        if (/loaded|complete/.test(document.readyState))
          fireContentLoadedEvent();
      }, 0);
      Event.observe(window, "load", fireContentLoadedEvent);
     } else {
        document.addEventListener("DOMContentLoaded",
        fireContentLoadedEvent, false);
    }
} else {
    document.write("<script id=__onDOMContentLoaded defer src=//:></script>");
    $("__onDOMContentLoaded").onreadystatechange = function() {
      if (this.readyState == "complete") {
        this.onreadystatechange = null;
        fireContentLoadedEvent();
      }
    };
  }
})();

这段代码的作用是实现DOMContentLoaded事件,在IE下的实现方案是在页面中写入一个script并注册该script的onreadystatechange来实现的。这里用到了document.write,如果在引入脚本的时候使用defer,那么在IE下会发现整个页面都变成了空白。

You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

6 Comments »

 
  • 俺觉得Script DOM Element不错
    可是怎么知道脚本全部加载完了呢…

  • admin 说:

    使用script dom element确实不好控制脚本的顺序

  • […]        前文有谈到,浏览器是可以并发请求的,这一特点使得其能够更快的加载资源,然而外链脚本在加载时却会阻塞其他资源,例如在脚本加载完成之前,它后面的图片、样式以及其他脚本都处于阻塞状态,直到脚本加载完成后才会开始加载。如果将脚本放在比较靠前的位置,则会影响整个页面的加载速度从而影响用户体验。解决这一问题的方法有很多,在这里有比较详细的介绍(这里是译文和更详细的例子),而最简单可依赖的方法就是将脚本尽可能的往后挪,减少对并发下载的影响。 […]

  • Me will hit on you … » 到底iframe会不会阻塞后续文档资源的加载? 说:

    […] 无阻塞加载脚本[译]+示例 […]

  • 前端开发优化总结 – 断桥残雪部落格 说:

    […] 前文有谈到,浏览器是可以并发请求的,这一特点使得其能够更快的加载资源,然而外链脚本在加载时却会阻塞其他资源,例如在脚本加载完成之前,它后面的图片、样式以及其他脚本都处于阻塞状态,直到脚本加载完成后才会开始加载。如果将脚本放在比较靠前的位置,则会影响整个页面的加载速度从而影响用户体验。解决这一问题的方法有很多,在这里有比较详细的介绍(这里是译文和更详细的例子),而最简单可依赖的方法就是将脚本尽可能的往后挪,减少对并发下载的影响。 […]

  • fighterJon 说:

    可以通过脚本的onreadystatechange属性(非IE)或者为脚本添加onload方法(IE)来了保证script dom element的下载顺序
    this.domScript.onload = function(){ //非ie情况
    setTimeout(function(){
    _parent._Done()
    },1);
    };
    this.domScript.onreadystatechange = function(){ //ie情况
    if((’loaded’ === this.readyState || ‘complete’ === this.readyState) && !this.onloadDone){
    setTimeout(function(){
    _parent._Done()
    },1);
    }
    }
    类似于这样的链式调用

 

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="">