一、阻塞脚本

1、脚本位置

大多数浏览器使用单进程处理UI更新和Javascript执行等多个任务,而同一时间只能有一个任务被执行。不论Javascript代码是内联还是通过外部文件引入,当Javascript脚本被执行时,浏览器不能处理其他事情,等待脚本被执行完成后,浏览器才能继续解析页面。

因此,通常是将所有的<script>标签放在尽可能接近</body>标签的位置,以减少对整个页面下载解析的影响。

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8">
		<title></title>
		<link rel="stylesheet" type="text/css" href="style.css">
	</head>
	<body>
		<p>content</p>
		<script type="text/javascript" src="script.js"></script>
	</body>
</html>

2、打包脚本

由于每个<script>标签下载时会阻塞页面解析过程,因此限制<script>标签的总数也可以改善性能。如果脚本是通过外部文件引入的,每加载一个Js文件时发送的HTTP请求都会产生额外的性能负担,可以将多个文件合并成一个文件来减少性能损失。

http://www.example.com/combo?build/action/action-min.js&build/event/event-min.js

二、非阻塞脚本

非阻塞脚本是在等待页面加载完成后,再执行JavaScript脚本,这种脚本的加载和执行不会阻塞页面的加载解析。有以下几种方式可以实现:

1、延迟(Deferred)脚本

<script>标签上使用defer属性

<script type="text/javascript" src="script.js" defer></script>

defer属性表示此脚本可以稍后执行,此属性被大多数高版本浏览器支持。带有defer属性的<script>标签可以放在文档的任意位置,对应的脚本文件将在<script>标签被解析时开始下载,但代码不会被执行,直到DOM加载完成(onload事件句柄被调用之前)后执行。而且这种脚本文件下载时不会阻塞浏览器的其他处理过程,因此可以与页面的其他资源并行下载。

defer属性不能用在没有src属性的<script>标签上,如果在内联脚本上添加defer属性则不会有任何效果。

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>JavaScript</title>
	<script src="./script.js" defer></script>
</head>
<body>
	<script defer>
		//defer属性在此处不会生效
		console.log('inline defer');
	</script>
	<script>
		console.log('script');
		window.onload = function(){
			console.log('load');
		};
	</script>
</body>
</html>

./script.js

console.log('file defer');

结果:

2、动态脚本元素

可以使用DOM API动态创建<script>元素:

var script = document.createElement("script");
script.src = "dynamic.js";
document.querySelector('head').appendChild(script);

使用此种方式,无论创建的<script>标签位于何处,脚本的下载和执行都不会阻塞其他页面处理过程;脚本下载完成后会立即执行其中的代码,可以使用onload(IE:readystatechange)事件句柄来实现脚本下载执行完成后的操作。

var script = document.createElement("script");
script.src = "dynamic.js";
script.onload = function(){
	console.log('dynamic script loaded!');
};
document.querySelector('head').appendChild(script);
//代版本IE
script.onreadystatechange = function(){
	var state = script.readyState;
	if(state === 'loaded' || state === 'completed'){
		script.onreadystatechange = null;
		console.log('dynamic script loaded!');
	}
};

可以将动态加载脚本的代码抽取为一个函数:

function loadScript(url, callback){
	var script = document.createElement("script");
	script.src = url;
	if(script.readyState){
		//兼容低版本IE
		script.onreadystatechange = function(){
			var state = script.readyState;
			if(state === 'loaded' || state === 'completed'){
				script.onreadystatechange = null;
				callback();
			}
		};
	}else{
		script.onload = function(){
			callback();
		};
	}
	document.querySelector('head').appendChild(script);
}

下面的例子用来测试脚本的执行顺序:

<body>
	<script>
		console.log('script');
		window.onload = function(){
			console.log('window loaded!');
		};
	</script>
	<script>
		//此处省略了loadScript函数的定义
		loadScript('./dynamic.js', function(){
			console.log('dynamic.js loaded!');
		});
	</script>
</body>

dynamic.js

console.log('execute dynamic javascript');

结果:

  • 加载多个动态脚本

如果需要按顺序加载多个动态脚本,可以使用以下方式:

loadScript('script1.js', function(){
	loadScript('script2.js', function(){
		loadScript('script3.js', function(){
			console.log('loaded!');
		});
	});
});

更好的方式是将这些文件按照正确的次序连接成一个文件,一次下载所有代码。

动态脚本加载是非阻塞JavaScript下载中最常用的模式,因为它可以跨浏览器而且简单易用。

3、XHR脚本注入

可以使用XHR对象下载JavaScript文件,然后将代码注入到动态<script>元素中(相当于动态创建含有内联代码的<script>元素):

var xhr = new XMLHttpRequest();
xhr.open('get', 'script.js', true);
xhr.onreadystatechange = function(){
	var status = null;
	var script = null;
	if(xhr.readyState === 4){
		status = xhr.status;
		if(status >= 200 && status < 300 || status === 304){
			script = document.createElement('script');
			script.text = xhr.responseText;
			document.body.appendChild(script);
		}
	}
};
xhr.send();
  • 优点

    • 可以下载不立即执行(推迟执行)的JavaScript代码

    • 跨浏览器

  • 缺点

    • JavaScript文件必须与HTML页面在同一个域中,不能从CDN中下载,因此,大型网页通常不使用此技术

4、推荐的模式

推荐的向页面加载大量JavaScript的方法如下:

  • 引入动态加载JavaScript脚本所需的代码(例如上面的loadScript函数)

  • 加载其他的JavaScript代码

<script type="text/javascript" src="loader.js"></script>
<script type="text/javascript">
	loadScript('the-rest.js', function(){
		doSomething();
	});
</script>

建议将以上代码放置在</body>标签之前,因为这样可以确保JavaScript代码的运行不会影响页面其他部分的显示,而且JavaScript文件下载完成后,应用所需的DOM已经创建好了,可以避免使用额外的事件处理(例如:window.onload)来得知页面是否已准备好。

也可以直接将loadScript()函数嵌入到HTML页面中,这样可以避免一次HTTP请求。

5、非阻塞JavaScript加载库

  • LABjs

LABjs对加载过程进行更精细的控制,并尝试下载尽可能多的代码:

<script type="text/javascript" src="./LAB.src.js"></script>
<script type="text/javascript">
	$LAB.script('base.js')
		.script('script.js')
		.wait(function(){
			console.log('ok!');
		});
</script>

其中script()函数用来下载一个JavaScript脚本文件,wait()函数用来指定一个在脚本文件下载并执行完之后被调用的函数。

LABjs能够管理依赖关系,控制并行下载的动态脚本的执行顺序,它使用wait()涵数指定哪些文件应该等待其他文件。上面的例子中,base.js的代码不能保证在script.js之前执行,因此需要在第一个script()函数之后使用wait()函数:

$LAB.script('base.js').wait()
	.script('script.js')
	.wait(function(){
		console.log('ok!');
	});
  • LazyLoad

LazyLoad提供了一个更强大的loadScript()函数,它可以下载多个JavaScript文件,并保证它们在所有的浏览器上都能够按正确的顺序执行。

<script type="text/javascript" src="lazyload.js"></script>
<script type="text/javascript">
	LazyLoad.js(['base.js', 'script.js'], function(){
		console.log('ok!');
	});
</script>
LazyLoad.css('style.css', function(arg){
	console.log(arg.active);//true	
	console.log(this.foo);//bar
}, {active: true}, {foo: 'bar'});
参考资料
  • 《高性能Javascript编程》