转:无阻塞加载脚本/示例
随着越来越多的站点向”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中能解决阻塞问题。
技术
| 并行下载
| 可以跨域
| 存在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是一个好的选择。这种方式适用于所有的浏览器,而且没有跨域的限制,实现起来也非常的简单和易于理解。唯一的缺点是不能保证各个脚本的执行 顺序。如果需要加载多个有依赖关系的脚本,应该将这些脚本拼成一个来保证其按需要的顺序执行,或者使用别的技术。
具体示例
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); } } importScript(['/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
代码
<script src="http://ajax.googleapis.com/ajax/libs/prototype/1.6.0.3/prototype.js></script> <script src=" http:="" ajax.googleapis.com="" ajax="" libs="" ext-core="" 3.0.0="" ext-core-debug.js"="" defer=""></script> |
时间瀑布图
在该例子中,仅仅对第二个脚本即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下会发现整个 页面都变成了空白。
转自: