问题
由于浏览器多数使用单一进程来处理用户界面(UI)和JavaScript的执行,也就是说当浏览器在执行JavaScript代码的时候,不能做其他任何事情。一旦JavaScript执行的时间过长,由于浏览器无法响应用户的其他操作,就会给人“卡住”的感觉。
反应在代码上,也就是每次遇到<script>
标签,浏览器都会停下来等待脚本的下载、解析和执行。
之所以浏览器采用单进程来处理,是因为脚本执行的过程中随时有可能修改页面内容,所以必须等到脚本执行完毕之后才能进行UI的渲染。例如:
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
</head>
<body>
<div>
<script>
document.write(new Date().toDateString());
</script>
</div>
</body>
</html>
解决措施
脚本位置
一般我们加载脚本文件或CSS文件都是如下写法:
<hmtl>
<head>
<script type="text/javascript" src="script1.js"></script>
<script type="text/javascript" src="script2.js"></script>
<script type="text/javascript" src="script3.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div>Hello World</div>
</body>
</html>
理论上来说,把脚本和样式文件放在<head>
标签内加载有利于页面渲染和交互的正确性。
不过这样会造成严重的性能问题,前面我们知道浏览器遇到<script>
标签就会停下来等它们全部下载并执行完,之后才会继续渲染页面。而浏览器在解析到<body>
标签之后才会渲染页面的内容。
换句话说,将<script>
标签放在<head>
标签内加载可能会导致页面长时间显示空白页面,也无法与页面进行交互,给人的感觉就是加载十分缓慢。
虽然现代浏览器大都允许并行下载脚本文件,这是个好消息,但是脚本的下载过程依然会阻塞其他资源的下载,例如图片。而且,浏览器依然需要等待脚本全部执行完才能继续。所以问题并没有得到根本的解决。而事实上,由于JavaScript的单线程,这个问题很难得到根治。
不过,我们依然可以试图让这个问题在用户看起来不那么明显。
可以把<script>
标签放在<body>
标签的最底部,这样可以等到页面内容都加载出来再开始加载<script>
。
由于页面都已经加载出来了,所以给到用户的感觉就是“这个网页加载速度还行”,尽管此时可能脚本文件尚未完成加载与执行。
以上面代码为例:
<hmtl>
<head>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div>Hello World</div>
<script type="text/javascript" src="script1.js"></script>
<script type="text/javascript" src="script2.js"></script>
<script type="text/javascript" src="script3.js"></script>
</body>
</html>
组织脚本
由于脚本文件的下载会阻塞页面的渲染,哪怕是脚本文件可以并行下载,可是HTTP请求所带来的性能开销也十分可观,于是尽量减少脚本文件的个数,或者说<script>
的个数将能够改善性能。
可以通过打包的方式,将多个脚本文件合成一个,从而减少<script>
标签的个数。
无阻塞的脚本
减少脚本文件大小并限制HTTP请求数只是性能优化的第一步。尽管下载单个较大的脚本文件只产生一次HTTP请求,却会阻塞浏览器一大段时间。为了避免这种情况的发生,我们可以逐步往页面中加载JavaScript文件。
脚本延迟(defer)
HTML为<script>
标签定义了一个扩展属性defer
,用以表明本元素所含脚本不会修改DOM,因此代码能够安全地延迟执行。
带有defer
属性的<script>
可以放置在文档的任何地方。它会在浏览器解析到该标签是开始下载,但并不会执行,直到DOM加载完成(DOMReady)。
动态脚本
所谓动态脚本就是利用了可以使用JavaScript来创建<script>
并加入到页面之中,这样就可以在需要时再加载脚本文件了。例如:
let script = document.createElement("script");
script.type = "text/javascript";
scirpt.src = "file1.js";
document.head.appendChild(script);