前记:js的出现给人们上网时的交互体验,但是一直有个地方被人们所诟病的是拖慢了网页的运行速度。
而且传统方式下,浏览器下载和运行js代码都是属于阻塞式的,会很消耗浏览器的运行时间。
为了提高网页运行速率,减少用于等待页面的时间 。很多人提出了很多种js加载和运行的方式。
以下方法出自《High Performance JavaScript》中文翻译版的《高性能JavaScript》。
基本工作原理:
1、大多数浏览器都是单线程处理UI更新和js运行等多个任务,也就说同一时间只能有一个任务被执行。
2、浏览器在遇到<script>标签时,都会停下来运行js代码,然后在进行页面解析和翻译页面。用src属性加载外部js文件时也会完全阻塞页面解析和用户交互。
3、浏览器在遇到<body>之前,不会渲染页面的任何部分。
注:目前IE8+,FF3.5+,Safari4+ , chrome2+(也就说现在(注明今年是2015年)的机会所有浏览器)都支持并行下载Javascript文件。但是JavaScript文件的下载还是会阻塞其他资源的下载,例如图片。
基于前人的工作经验,和本书中提出的方法,可以做的一些操作是:
(1)将Js脚本放在底部,</body>之前就好。(Yahoo优越性能小组关于Javascript第一条定律)
(2)减少<script>标签数。
(3)打包js文件组。
对于外部js文件,每个HTTP请求都会产生额外的性能负担。比如下载一个100KB的文件要比下载四个25KB的文件要快。
可以利用打包工具打包js文件,Yahoo提供了一个实时工具。(Yahoo!combo handler.)
非阻塞脚本
非阻塞的思路是:等页面都加载完成以后,在加载js源码。在window的load事件发生以后再开始下载代码。
(1) 延迟脚本defer
defer属性是HTML4中给出的,指明元素中所包含的脚本不打算修改DOM,代码可以稍后执行。
特性是:js文件在script被解析时下载,但代码不被执行,直到DOM都加载完成(在window的load事件句柄被调用之前执行)。
而且在下载js文件的时候,不阻塞其他处理过程,可以和页面的其他资源一起并行下载。
但是支持defer的浏览器不多。
注:笔者自己测试下来,只有IE(测试的是IE8)支持defer,FireFox(测试的是FF42),chrome(测试的是chrome46),Opera(测试的是Opera33),Safari(测试的是Safari5.34)都不支持 所以这个方法虽然简单,但是浏览器们都不支持啊!
(2)动态脚本元素Dynamic Script Elements(非阻塞js下载中最常用的模式)
<script> var script = document.createElement("script");
script.src = "f1.js";
script.type = "text/javascript";
document.getElementsByTagName("head")[0].appendChild("script");
</script>
无论在何处启动下载,文件的下载和运行都不会阻塞其他页面处理过程。甚至可以将代码放在<head>内部而不会对其余部分的页面代码造成影响(除了用于下载文件的HTTP连接)。
当加载js文件采用DSE(动态脚本元素的简称)时,下载完成后返回的代码会立即执行(Opera和FireFox除外,他们会等待此前的所有动态脚本执行完毕)。所以,当返回代码是自执行代码就ok。
但是如果返回的脚本代码只是其他脚本的调用的接口,则会出现问题。(什么问题呢??待追)
所以在这种情况下,需要自己去追踪脚本的处理进度(事件监听),是否已经准备好来使用。
IE浏览器可以发出readystatechange事件,还给script元素提供了readyState属性,有5个值:
- "uninitialized":默认状态,还未开始初始化
- "loading":下载已经开始
- "loaded":下载完成
- "interactive":下载完成但尚不可用
- "complete": 所有数据都已准备好,可以使用
一旦监听到上面的5个值之一,在使用完readystatechange以后,就需要删除readystatechange事件句柄。(script.readystatechange==null,保证事件不会被处理两次)
在FireFox,Safari3+,Opera中,会发出load事件。
//兼容各个浏览的加载函数 function loadScript( url , callback ) { var script = document.createElement("script"); script.src = "text/javascript"; if( script.readyState ) { //IE script.readystatechange = funciton() { if( script.readyState == "loaded" || script.reayState == "complete") { script.readystatechange = null; callback(); } } } else { //非IE script.onload = function() { callback(); } } script.src = url; document.getElementByTagName("head")[0].appendChild(script); }
用这种方式加载js文件,浏览器不能保证文件加载顺序(Firefox和Opera除外)。
要保证文件执行顺序,可以使用嵌套的callback,但是文件过多的情况下不推荐使用。如果文件顺序很重要而且文件很多,则可以将文件打包成一个大文件。
(3)XHR脚本注入方式(大型网页不采用)
方法是:
第一:创建XHR对象注入页面中
第二:下载js文件
第三:动态创建script注入页面中
var xhr = window.XMLHTTPRequest? new window.XMLHTTPRequest():new ActiveXObject("Microsoft.XMLHTTP"); xhr.open("f1.js",'get',false); xhr.send(); xhr.onreadystatechange = function() { if( xhr.readyState == 4 ) { if( xhr.status >= 200 && xhr.status < 300 || xhr.status == 304 ) { var script = document.createElement("script"); script.type = "text/javascript"; script.text = xhr.responseText; document.body.appendChild(script); } } }
优点:
- 可以下载不立即执行js代码,可以推迟执行,直到一切准备好。
- 在所有浏览器中,都不会引发异常。
缺点:
- 无法跨域实现,js文件与页面必须在同一个域内。
该书推荐的nonblocking Pattern(非阻塞模式)
加载和运行js文件分两步:
第一步:将极必要且很小的code采用动态加载js的方式实现,主要保证加载的js内容尽量小,小到只剩下loadScript()函数。
第二步:然后加载其余的初始化js代码
<script src = "loadScript.js" type = "text/javascript">
<script>
loadScript("the-rest.js",function(){ application.init(); });
</script>
将这段代码放置在</body>之前,好处是:不会阻塞页面其他部分的显示;当第一部分js文件loading完,所有需要的DOM节点,都已经创建完成,并准备好被访问。
可以避免额外的事件处理window.onload,来告知页面是否准备好了。
另外一种选择是,直接将loadScript函数嵌入到页面中,避免HTTP请求。(如果采用这种方式,建议使用"YUI Compressor"或类似工具将脚本压缩到最小字节尺寸)。一旦页面初始化代码下载完成,还可以使用loadScript()函数加载页面所需的额外功能函数。
其他(一些开源的插件)
(1)YUI3:由一段很小的初始化代码组成,用于下载其余的功能代码。
需要在页面中加载YUI的种子文件。(6KB,gzipped)
<script type = "text/javascript" src = "http://yui.yahooapis.com/combo?3.0.0/build/yui/yui-min.js"></script>
(2)The LazyLoad (精缩以后只有1.5KB)
<script type = "text/javascript" src = "lazyload-min.js"></script> <script> LazyLoad.js("f1.js",function(){ //多个就参数文件数组:["f1.js","f2.js"...] application.init(); }) </script>
能够下载多个js文件,并能够保证在所有的浏览器上按照正确顺序执行。
注:即使使用非阻塞方式下载,仍然建议较少js文件数量,因为每次下载一个文件仍然是一个单独的HTTP请求,回调函数callback()在所有文件下载并执行完成后执行。
(3)LABjs (精缩后4.5KB)(链式操作)
常用接口:
script('f1.js') : 下载f1文件
wait(fn) : 在js文件下载并执行完成以后,执行fn函数
//以这种连续方式下载f1,f2两个js文件,无法保证f1先执行。 LABjs.script("f1.js").script("f2.js").wait(function() { application.init(); }); //可以保证f2在1之后执行,但是两者是并行下载的 LABjs.script("f1.js").wait().script("f2.js").wait(function() { application.init(); });
LABjs最大的特色就是:管理依赖关系。
其中的wait()函数可以指定哪些文件应该等待其他文件执行完在执行。
后记:以上这些纯粹都是一些理论分析,笔者还没有真正实践测试过。所谓实践出真知,等笔者学会了后台技术(捂脸。。。)再依次好好测试各种方法的优缺点及性能。