在面试的时候,经常会遇到一道经典的面试题:
如何优化网页加载速度?
常规的回答中总会有一条:
把 css 文件放在页面顶部,把 js 文件放在页面底部。
那么,为什么要把 js 文件放在页面的最底部呢?
我们先来看下这段代码:
<!DOCTYPE html>
<html lang="zh">
<head>
<title>Hi</title>
<script>
console.log("Howdy ~");
</script>
<script src="https://unpkg.com/vue@3.2.41/dist/vue.global.js"></script>
<script src="https://unpkg.com/vue-router@4.1.5/dist/vue-router.global.js"></script>
</head>
<body>
Hello ~
</body>
</html>
他的执行顺序是:
Howdy ~
Hello ~
script 加载逻辑
浏览器的解析规则是:如果遇到 script
标签,则暂停构建 DOM
,转而开始执行 script
标签,如果是外部 script[2],那么浏览器还需要一直等待其「下载」并「执行」后,再继续解析后面的 HTML。
如果请求并执行「vue.global.js」需要 3 秒,「vue-router.global.js」需要 2 秒,那么页面中的 Hello ~
,则至少需要 5 秒以上才会展示出来。
可以看到,script 标签会阻塞浏览器解析 HTML,如果把 script
都放在 head
中,在网络不佳的情况下,就会导致页面长期处于白屏状态。
在很久以前,一般都是将这些外联脚本,放在 body
标签的最后面,确保先解析展示 body
中的内容,然后再一个个请求执行这些外联脚本。
那有没有其他更优雅的解决方案呢?
答案是肯定的,现在 script
标签新增了 2 个属性:defer
和 async
,就是为了解决此类问题,提升页面性能的。
<script defer>
先看一下 MDN 上的解释:
这个布尔属性被设定用来通知浏览器该脚本将在文档完成解析后,触发 DOMContentLoaded 事件前执行。
有 defer 属性的脚本会阻止 DOMContentLoaded 事件,直到脚本被加载并且解析完成。
文档是直接总结了他的特性,我们先看看下面的代码,展开说说细节,加深一下理解。
<!DOCTYPE html>
<html lang="zh">
<head>
<title>Hi</title>
<script>
console.log("Howdy ~");
</script>
<script defer src="https://unpkg.com/vue@3.2.41/dist/vue.global.js"></script>
<script defer src="https://unpkg.com/vue-router@4.1.5/dist/vue-router.global.js"></script>
</head>
<body>
Hello ~
</body>
</html>
他的执行顺序是:
Howdy ~
Hello ~
script defer 加载逻辑
如果在 script
标签上设置了 defer
属性,那么在浏览器解析到这里时,会默默的在后台开始下载此脚本,并继续解析后面的 HTML,并不会阻塞解析操作。
等到 HTML 解析完成之后,浏览器会立即执行后台下载的脚本,脚本执行完成之后,才会触发 DOMContentLoaded
事件。
看起来还是蛮好理解的吧?咱们再来讨论 2 个小细节:
Q1: 如果 HTML 解析完成之后,设置了 defer
属性的脚本还没下载完成,会怎样?
A1: 浏览器会等脚本下载完成之后,再执行此脚本,执行完成之后,再触发 DOMContentLoaded
事件。
Q2: 如果有多个设置了 defer
属性的脚本,那浏览器会如何处理?
A2: 浏览器会并行的在后台下载这些脚本,等 HTML 解析完成,并且所有脚本下载完成之后,再按照他们在 HTML 中出现的相对顺序执行,等所有脚本执行完成之后,再触发 DOMContentLoaded
事件。
最佳实践:
建议所有的外联脚本都默认设置此属性,因为他不会阻塞 HTML 解析,可以并行下载 JavaScript 资源,还可以按照他们在 HTML 中的相对顺序执行,确保有依赖关系的脚本运行时,不会缺少依赖。
在 SPA 的应用中,可以考虑把所有的 script
标签加上 defer
属性,并且放到 body
的最后面。在现代浏览器中,可以并行下载提升速度,也可以确保在老浏览器中,不阻塞浏览器解析 HTML,起到降级的作用。
注意:
defer
属性仅适用于外部脚本,如果 script
脚本没有 src
,则会忽略 defer
特性。defer
属性对模块脚本(script type='module'[4])无效,因为模块脚本就是以 defer
的形式加载的。<script async>
按照惯例,先看一下 MDN 上的解释:
对于普通脚本,如果存在 async 属性,那么普通脚本会被并行请求,并尽快解析和执行。
对于模块脚本,如果存在 async 属性,那么脚本及其所有依赖都会在延缓队列中执行,因此它们会被并行请求,并尽快解析和执行。
该属性能够消除解析阻塞的 Javascript。
解析阻塞的 Javascript 会导致浏览器必须加载并且执行脚本,之后才能继续解析。
感觉这段描述的已经蛮清晰了,不过咱们还是先看看下面的代码,展开说说细节,加深一下理解。
<!DOCTYPE html>
<html lang="zh">
<head>
<title>Hi</title>
<script>
console.log("Howdy ~");
</script>
<script async src="https://google-analytics.com/analytics.js"></script>
<script async src="https://ads.google.cn/ad.js"></script>
</head>
<body>
Hello ~
</body>
</html>
他的执行顺序是:
Howdy ~
Hello ~
script async 加载逻辑
浏览器在解析到带有 async
属性的 script
标签时,也不会阻塞页面,同样是在后台默默下载此脚本。当他下载完后,浏览器会暂停解析 HTML,立马执行此脚本。
看起来还是蛮好理解的吧?咱们再来讨论 2 个小细节:
Q1:如果设置了 async
属性的 script
下载完之后,浏览器还没解析完 HTML,会怎样?
A1:浏览器会暂停解析 HTML,立马执行此脚本,等执行完之后,再继续解析 HTML。
Q2:如果有多个 async
属性的 script
标签,那等他们下载完成之后,会按照代码顺序执行吗?
A2:不会。执行顺序是:谁先下载完成,谁先执行。async
的特点是「完全独立」,不依赖其他内容。
最佳实践:
当我们的项目,需要集成其他独立的第三方库时,可以使用此属性,他们不依赖我们,我们也不依赖于他们。通过设置此属性,让浏览器异步下载并执行他,是个不错的优化方案。
注意:
async
特性仅适用于外部脚本,如果 script
脚本没有 src
,则会忽略 async
特性。defer
script
。DOMContentLoaded
事件。defer
属性对模块脚本无效。src
引用的 script
)。async
script
下载完成后,会立即中断浏览器解析 HTML,并执行此 script
。DOMContentLoaded
事件和 script
脚本无相关性,无法确定他们的先后顺序。另外:async
和 defer
之间最大的区别在于它们的执行时机。
你有没有想过,如果一个 script
标签同时设置 defer
和 async
,浏览器会如何处理?
先说结论:从表现形式上来说,async
的优先级比 defer
高,也就是如果同时存在这 2 个属性,那么浏览器将会以 async
的特性去加载此脚本。
这主要分 2 种情况:
如果是「普通脚本」,浏览器会优先判断async
属性是否存在,如果存在,则以async
特性去加载此脚本,如果不存在,再去判断是否存在defer
属性。
如果是「模块脚本[5]」,浏览器会判断async
属性是否存在:
defer
属性是无效的。最后,用一张图概括一下这两个属性的加载模式吧:
defer 和 async 的加载模式
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8