原文:JavaScript | async、defer与动态加载
前言:一直不太理解script标签中的async、defer相关的关键字,所以打算写一篇随笔记录下这块的知识点
常见的script标签有两种,一般分为内联脚本以及外联脚本:
-
内联:
html<script> alert('明华大鼬'); </script>
-
外联:
html<script scr="../a.js"></script>
是上面这种引入方式都算是直接引入的,是同步加载的脚本,而在html解析的过程中遇到同步脚本,停止解析,加载脚本,执行脚本,完成后再继续解析html文档。
同步脚本的解析:
注意:这也是为什么不推荐把
script
标签写在<head>
中,对于需要很多 JavaScript 的页面,这会导致页面渲染的明显延迟,在此期间浏览器窗口完全空白,为解决这个问题,现代 Web 应用程序通常将所有 JavaScript 引用放在<body>
元素中的页面内容后面。------《JavaScript高级程序设计(第4版)》
defer
从HTML 4.01开始<script>
元素定义了一个叫 defer
的属性
- 只对外部脚本文件有效果 (外部资源中不可以使用
document.write
) - 表示脚本立即下载(异步,不影响文档的加载,不会造成阻塞),但延迟到文档完全被解析和显示之后再执行
- 在DOMContentLoaded事件之前执行
- HTML5 规范要求defer脚本应该按照它们出现的顺序执行
html
<script scr="../a.js" defer></script>
<script scr="../b.js" defer></script>
上面这种情况b.js就会在a.js之后执行
async
HTML5为<script>
元素定义了 async
属性
-
只对外部脚本文件有效果 (外部资源中不可以使用
document.write
) -
表示脚本立即下载(异步,不影响文档的加载),异步下载完Js文件之后,立即执行,阻塞了文档的解析
-
在一些情况下,如果Js文件还没下载完成,而此时文档已经加载完毕后,也是立即执行,但是此时不会阻塞文档的解析
-
属性为
async
的脚本并不保证能按照它们出现的次序执行,完全依赖于网络传输结果,谁先到执行谁 -
通过2、3两个点可以看出使用
async
属性的外部脚本执行时间不确定,如果在异步(async) js 脚本中获取某个 DOM 元素,有可能获取到也有可能获取不到,所以不推荐通过此种方式引入的js文件中包含操作dom元素的代码 -
一定会在页面的
load
事件前执行,但可能会在DOMContentLoaded
事件之前或之后执行
async与defer的区别
async
标志的脚本文件一旦加载完成,会立即执行,会在页面的load
事件前执行,但可能会在DOMContentLoaded
事件之前或之后执行,对于那些可以不依赖任何脚本或不被任何脚本依赖的脚本来说却是非常合适的,最典型的例子:Google Analytics- 使用了
defer
属性的脚本文件,在DOMContentLoaded
事件之前执行(load
事件的执行事件也是在DOMContentLoaded
事件之前) - HTML5 规范要求defer脚本应该按照它们出现的顺序执行,async脚本并不保证能按照它们出现的次序执行,完全依赖于网络传输结果,谁先到执行谁
预解析
chrome浏览器有预解析的机制,会在解析文档之前查看有没有外部文件,如果有就交给下载进程同步下载
当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析 HTML 文件中包含的 JavaScript、CSS 等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件。
预解析和 async 的区别:
- 下载时机
- 预解析线程可以在解析文档线程之前就解析到HTML页面是否有引用外部资源,如果有的话,就立即交给下载进程下载
- async 只有在解析文档线程解析到有外部资源,才去发起下载
- 作用对象
- 预解析对js文件,和css文件都有效
- async 只能用于js资源
- 预解析存在的意义
- 当文档解析到js资源的时候,解析文档的工作会被阻塞。如果js资源是外部引入的,阻塞的时间就包括js资源下载的时间
- 预解析就是对于没有添加 async 属性的script的标签,做的优化
预解析和 async 的相同点:
- 下载js资源的时候,不会阻塞文档的解析
动态加载脚本
除了<script>
标签,还有其他方式可以加载脚本。
以前没有async以及defer的时候,此时一般都是使用JavaScript提供DOM相关的API动态的向页面上添加指定的脚本文件:
javascript
let script = document.createElement('script');
script.src = 'a.js';
document.head.appendChild(script);
一些注意的点:
-
默认情况下这种方式创建的script标签是异步加载的,相当于async的形式,在页面执行到添加script标签之前是不会发送请求的。
-
早期一些浏览器不支持async属性,如果要统一动态脚本的加载行为,可以明确将其设置为同步加载:
javascriptlet script = document.createElement('script'); script.src = 'a.js'; script.async = false; document.head.appendChild(script);
-
通过动态加载的方式加载脚本可能会影响性能,可以在文档头部显示声明,提前告诉浏览器此文件的存在
html<link rel="preload" href="abc.js">