文章目录
默认script标签是同步执行,因此会发生阻塞
浏览器解析html文件时,从上向下解析,解析到DOM中的script时会暂停DOM构建,在脚本加载并执行完毕后才会继续向下解析
因此可以看到,JS脚本存在会阻塞DOM解析的问题进而影响页面渲染速度
实验
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>js单线程阻塞</title>
</head>
<body>
<h1>111</h1>
<script src="index.js"></script>
<h1>222</h1>
</body>
</html>
index.js
const startTime = new Date().getTime()
let endTime = ''
do {
endTime = new Date().getTime()
} while (endTime - startTime <= 2000)
在浏览器中打开可以看到,script
之上的dom渲染完成,在加载和运行 script
时耗费了 2s后,script
后面的的 dom 才加载
解决方案
1. script 放在 body 最下方
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>js单线程阻塞</title>
</head>
<body>
<h1>111</h1>
<h1>222</h1>
<!-- script 放在 body 最下方 -->
<script src="index.js"></script>
</body>
</html>
script 放在 body 最下方,所有的dom加载完成后才加载、执行script,因此不会阻塞页面的渲染,如下图
2. 使用 async 异步加载 script
dom
解析时,遇到设置了async
的脚本,就会在后台进行下载,但是并不会阻止dom
的渲染。
当页面解析并且渲染完毕后,在Load 事件触发前执行,async脚本的加载不计入DOMContentLoaded事件统计
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>js单线程阻塞</title>
</head>
<body>
<h1>111</h1>
<!-- 使用 async 异步加载 script -->
<script async src="index.js"></script>
<h1>222</h1>
</body>
</html>
index.js
const startTime = new Date().getTime()
let endTime = ''
do {
endTime = new Date().getTime()
} while (endTime - startTime <= 2000)
debugger
下图可以看到,在执行断点前,dom已经渲染完毕,不会阻塞页面
3. 使用 defer 异步加载 script
dom
解析时,遇到设置了defer
的脚本,就会在后台进行下载,但是并不会阻止dom
的渲染,当页面解析并且渲染完毕后。
会等到所有的defer
脚本加载完毕并按照顺序执行,执行完毕后会触发DOMContentLoaded事件。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>js单线程阻塞</title>
</head>
<body>
<h1>111</h1>
<!-- 使用 defer 异步加载 script -->
<script defer src="index.js"></script>
<h1>222</h1>
</body>
</html>
下图可以看到,在执行断点前,dom已经渲染完毕,也不会阻塞页面
4. WebWorker
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>js单线程阻塞</title>
</head>
<body>
<h1>111</h1>
<!-- WebWorker -->
<script>
const worker = new Worker('./index.js')
worker.postMessage("兄弟,帮我运行下这个脚本")
worker.onmessage = (e) => {
console.log(e);
}
</script>
<h1>222</h1>
</body>
</html>
index.js
const startTime = new Date().getTime()
let endTime = ''
do {
endTime = new Date().getTime()
} while (endTime - startTime <= 2000)
onmessage = (e) => {
console.log(e);
postMessage("好的大兄弟")
}
下图可以看到,在执行断点前,dom已经渲染完毕,不会阻塞页面
总结
问题
默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>
标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。
如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。这显然是很不好的体验,所以浏览器允许脚本异步加载,下面就是两种异步加载的语法。
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
上面代码中,<script>
标签打开defer
或async
属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。
defer
与async
的区别是:defer
要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer
是“渲染完再执行”,async
是“下载完就执行”。另外,如果有多个defer
脚本,会按照它们在页面出现的顺序加载,而多个async
脚本是不能保证加载顺序的。
解决方案
- 将script 放在 body 最下方
- 使用 async 异步加载 script
- 使用 defer 异步加载 script
- WebWorker
async、defer 推荐应用场景
-
如果你的JS代码依赖于页面中的DOM元素,或者被其他脚本文件依赖,应当使用
defer
; -
如果你的脚本并不关心页面中的DOM元素,并且也不会产生其他脚本需要的数据,可以使用
async
; -
如果不太确定的话,选择
defer
会比async
更靠谱