默认阻塞的脚本JS在浏览器上的性能,可以认为是开发者所面临的最严重的可用性问题。JS的阻塞特性使得浏览器在执行JS代码时不能同时做其他任何事情,而大多数浏览器使用单一的进程来处理用户界面(UI)刷新和JS脚本执行,所以同一时刻只能做一件事,JS执行过程耗时越久,浏览器所等待的响应时间越长。在这篇文章中,你将会学到浏览器脚本文件下载和执行的阻塞特性和如何对其进行优化。使用script元素的属性以及一些常用手段使脚本无阻塞加载。本篇文章参考了《高性能javascript》及《javascript模式》里的内容。
script标签
script标签每次出现都会霸道的让页面等待脚本的解析和执行,同样,当使用script的src属性加载页面时,浏览器必须先花时间下载外链文件中的代码,然后解析并执行。在这个过程中,页面渲染和用户交互是完全被阻塞的。
浏览器之所以产生这样的行为,是因为当前HTML页面无从知晓JS的动作:JS可能会向document里添加内容、引入其它元素、甚至关闭标签。
所以浏览器会先(下载和)执行JS代码,然后才解析和渲染页面。
请看下面的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
hello world
<script>
while (1){}
</script>
<div ></div>
</body>
</html>
不要妄想页面会输出hello world,然后由于循环而“卡壳”。事实上页面不会输出任何东西,因为脚本的执行一直占有浏览器进程,使得界面无法渲染。
优化
想要使页面得到更快的渲染,一是优化脚本的执行速度,例如使用观察者模式减少初始化代码的执行数量,这并不在这篇文章的阐述范围之内。请牢记,默认状态下,所有的脚本必须被执行后,页面才会开始渲染。即在浏览器解析页面之前,须先读取并执行脚本。我们现在试着对脚本的下载过程进行优化。
现代浏览器都允许并行下载JS文件,但JS文件的下载过程仍然会阻塞其他比如图片资源的下载。尽管JS文件的下载并不会相互影响,但是浏览器会等待全部的JS代码下载完成才执行之。
优化的关键在于使各种资源的下载并行执行,下面是分条列项的几点注意事项:
1. 雅虎特别性能小组提出的优化JS的首要规则:将脚本放在底部
这样做可以防止脚本代码的下载与执行阻塞页面其它资源,比如图片的下载(下载往往需要更多的时间),以尽量减少对整个页面下载的影响。
如果把脚本文件放在头部,那么需要等到所有脚本下载并执行完毕后才能下载图片等其他资源,这是多么的可怕啊。但如果我们把脚本放在body标签的尾部,则可以是其它资源的下载和脚本的下载与执行并发进行,能够大大加快页面的下载速度。
2. 减少script标签的数量,减少延时
3. 不要把内嵌脚本紧跟在link标签后面
<link ref="test.css" rel="stylesheet">
<script>
// do something
</script>
这样做会导致页面阻塞去等待样式表的下载,因为需要确保内嵌脚本在执行时能获得最准确的样式信息。
4. 减少外链脚本文件的数量——将多个外链JS文件合并成一个以减少HTTP开销
无阻塞的脚本无阻塞脚本意味着脚本的下载和执行不阻塞其他资源的下载和页面的解析
script属性
可以通过设置script标签的defer和async属性使其拥有不阻塞的特性,它们仅对外部链接的script有效。
带有async或者defer的script都会立刻下载并不阻塞页面解析,它们的不同之处在于script执行的时机。
defer
<script src="test.js" defer></script>
会确保按脚本在页面中出现的顺序来执行,它们执行的时机是在页面解析完后, DOMContentLoaded事件之前,这时脚本可以获得页面的所有元素。如下事例代码:
html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<script src="test.js" defer></script>
<script src="test2.js" defer></script>
</head>
<body>
<div id='test' >hello world</div>
<script>
alert('阻塞加载的script');
</script>
<script>
window.onload =function () {
alert('页面加载完毕');
};
</script>
</body>
</html>
test1.js
var div = document.getElementById('test');
alert(div.innerHTML);
test2.js
alert('我是test2.js');
依次输出:
加载阻塞的javascript
hello world
我是test2.js
页面加载完毕
async
async是html5中的新属性。带有async的script,一旦下载完成就开始执行(当然是在window的onload之前)。这意味着这些script 可能不会按它们出现在页面中的顺序来执行,如果你的脚本互相依赖并和执行顺序相关,就有很大的可能出问题。由于这依赖于下载文件的大小,我就不进行测试了。
动态脚本元素
类似于JSONP的形式,动态创建脚本元素并加载到页面中。这种技术的重点在于该元素被添加到页面时开始下载,文件的下载和执行过程不会阻塞页面其它进程。
<script>
var script = document.createElement('script');
script.src = 'test2.js';
document.getElementsByTagName('head')[0].appendChild(script);
</script>
但值得注意的有两点:
1. 把新创建的script标签添加到head标签里可以防止“操作已终止错误”
2. 下载完成后,返回的代码会立即执行,所以你需要通过onload事件控制加载顺序和依赖关系
script.onload = function () {
}
XMLHttpRequest也是一种办法,但因为它无法实现跨域,不能从CDN上获取文件,因此不常使用。而像CSS等文件,已经是并行下载,不会阻塞页面其它进程,故没有必要动态加载CSS文件。
小结
虽然无阻塞脚本技术可以动态加载很多文件,但为了减小HTTP的开销,还是需要尽量减少文件数。