浏览器在解析HTML的过程中,遇到了script元素时的默认行为:
- 暂停HTML解析,下载JavaScript代码,并且执行JavaScript的脚本;
- 等到JavaScript脚本执行结束后,才会继续解析HTML,构建DOM树;
原因:
- JavaScript的作用之一就是操作DOM,并且可以修改DOM;
- 如果我们等到DOM树构建完成并且渲染再执行JavaScript,会造成严重的回流和重绘,影响页面的性能;
- 所以在遇到JavaScript元素时,有限下载和执行JavaScript代码,再继续构建DOM树;
但是在目前的开发模式中(如:Vue、React)中,脚本往往比HTML页面更”重“,需要的处理时间也就更长,因此也带来了新的问题:
- 阻塞渲染:脚本执行期间,浏览器会暂停HTML解析,同时阻塞页面渲染,如果脚本执行时间较长(如加载外部资源、执行复杂计算),会导致页面长时间处于白屏或卡顿状态;
- 依赖关系处理:如果脚本依赖外部资源(如API数据、其他脚本),当外部资源加载较慢时,会进一步延长脚本执行时间,加剧阻塞问题;
为了解决这个问题,script元素提供了两个属性:defer 和 async。
defer (延迟执行):
defer属性告诉浏览器不要等待脚本下载,而是继续解析HTML,构建DOM Tree:
-
脚本会由浏览器来进行下载,但是不会阻塞DOM Tree的构建过程;
-
如果脚本提前下载好了,它会等待DOM Tree构建完成,在DOMContentLoaded事件之前先执⾏defer中的代码,所以DOMContentLoaded总是等待defer中的代码先执行完成;
// js/defer-demo.js console.log("defer-demo execute~");
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>defer在script标签中的使用</title> <script defer src="./js/defer-demo.js"></script> <script> window.addEventListener("DOMContentLoaded", () => { console.log("DOMContentLoaded"); }); </script> </head> <body> <ul> <li>li</li> <li>li</li> <li>li</li> </ul> </body> </html>
-
顺序执行:多个带defer的脚本会保持正确的顺序执行。
<script defer src="./js/defer-demo1.js"></script> <script defer src="./js/defer-demo2.js"></script> <script> window.addEventListener("DOMContentLoaded", () => { console.log("DOMContentLoaded"); }); </script>
从某种角度上来说,defer可以提高页面的性能,并且推荐放到head元素中。
async的作用:
async 特性与 defer 有些相似,也能够让脚本不阻塞页面;
async 脚本的下载和执行都是独立的:
-
异步加载并执行:async脚本会在下载和立即执行,不能保证在DOMContentLoaded之前或者之后执⾏(执⾏时会阻塞DOM Tree的构建);
// js/async-demo.js console.log("async-demo execute~");
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>async在script标签中的使用</title> <script async src="./js/async-demo.js"></script> <script> window.addEventListener("DOMContentLoaded", () => { console.log("DOMContentLoaded"); }); </script> </head> <body> <ul> <li>li</li> <li>li</li> <li>li</li> </ul> </body> </html>
DOMContentLoaded async-demo execute~
<!-- 模拟需要渲染的HTML内容较多时 --> <script> let count = 0; for (let i = 0; i < 10000; i++) { count += 1; } console.log(count); </script>
10000 async-demo execute~ DOMContentLoaded
-
不保证顺序:多个
async
脚本的是独立下载、独立运行的,不会等待其他脚本。
总结:
<script>
标签的 defer
和 async
属性⽤来控制外部脚本⽂件的加载和执⾏⽅式,它们对于改善⻚⾯加载速度⾮常有帮助。
属性 | 加载是否阻塞HTML解析 | 执行时机 | 执行顺序 | 适用场景 |
---|---|---|---|---|
defer | 否(异步) | DOM Tree构建完成后,DOMContentLoaded事件之前执行 | 严格按文档顺序 | 需要等待DOM就绪的脚本(如操作DOM的库或代码)。 脚本之间依赖性强,需按顺序执行。 |
async | 否(异步) | 下载完立即执行 | 不确定(谁先下载完谁先执行) | 独立脚本(如分析工具、广告、统计代码); 脚本之间没有依赖关系,或依赖关系可通过其他方式处理。 |
图示流程:
HTML解析
├─ 遇到普通<script> → 暂停解析,下载并执行 → 继续解析
├─ 遇到async脚本 → 后台下载,下载完立即执行(可能中断解析)
└─ 遇到defer脚本 → 后台下载,HTML解析完后再按顺序执行
在现代化框架开发过程中,往往不需要我们⾃⼰来配置async或者defer,在使⽤脚⼿架或者⾃⼰搭建的webapck或 者vite项⽬进⾏打包时,它会根据需要帮我们加上defer属性,某些情况下我们想要进⾏性能优化时,也可以⼿动的 加上async属性(例如⼀些第三⽅的分析⼯具或者⼴告追踪脚本)。
注意事项:
- 如果同时使用
async
和defer
,现代浏览器会优先采用async
。 - 内联脚本(无
src
)的async
和defer
会被忽略。