async 和 defer 对业务页面优化的价值
关于 async 和 defer 的执行时机到底在什么时候。网上的文章很多不太严谨。
例如经常提到的“async 是在加载完成后就立刻执行”。这句就明显存在误导性。“难道 defer 就不是下载完成后立刻执行吗?”。另外,所谓“立刻执行”也不严谨,理论上浏览器解析 dom 是个同步单线程的过程,在 dom 解析完毕之前本来就没有机会去“立刻执行”别的东西。
因此,本文重新梳理并实验 async 和 defer 脚本的执行时机问题,并给出结论。
结论
- 首先我们给出一个关于 module 和 defer 的结论—module 的加载时机和顺序是跟 defer 一样的。即解析 dom 完毕后执行,但必须按照声明顺序执行—即如果 b 在 a 后面声明,如果 a 还没执行,则 b 不敢执行。
- 关于 async,其执行时机也是在 dom 解析完毕后执行,但他没有所谓的“声明依赖”,他每个 js 都是独立的个体。即:若 b 有机会执行的时候,他不需要考虑去等待 a 执行。
- 当多条混合声明语句同时存在的时候(例如本文的实验)。到底谁先执行,谁后执行,以及 async 到底穿插到谁中间执行呢?答案是:对 async 来说,谁先下载回来且浏览器有空闲时间就立刻执行;对 defer 和 module 也是同样道理,谁先下载回来且浏览器若有空闲时间就立刻执行,但他要看看他前面声明的 js 是否还没执行,若前面的 js 还未执行则他会等待。
何为浏览器空闲
浏览器的空闲时间一般在本文(浏览器初次打开页面)场景下,通常就是 dom 解析完毕后—即浏览器从上往下解析 html 内容并解析其中的 同步 css/js,并到达 html 尾部之后。
对于浏览器主流时间来说,就是当 document.readyState 状态从 loading 变成了 interactive 的时候,就代表 dom 解析完毕,此时他就要开始执行你的 “async/defer/module 脚本”了。
而 DOMContentLoaded 有个特点:他必须会等待你页面的 defer 和 module 脚本加载完毕后才触发。可能是因为他认为你页面的核心业务逻辑(例如一些动态 dom 构建)可能会写在 defer 和 module 脚本中,甚至你的 defer 脚本中可能还会去监听 DOMContentLoaded 事件,所以他要等你们的 defer/module 脚本执行完毕后,再去触发 DOMContentLoaded。
实验
我们把 defer、module、async 都放在一起进行实验。编写如下代码:
| 1 | <script type="module" src="./tmp/module2.js"></script> | 
之后我不停刷新页面,结果如下:
1 次结果

得出的结论:
- 多个 async 之间无固定顺序。每次执行不一定保证顺序。
- async 可能在 DOMContentLoaded 前也可能在 DOMContentLoaded 后。 即 DOMContentLoaded 不会严格等待 async 脚本。
2 次结果

得出结论:async 即使声明的时机比 defer 靠后,但执行的时候也可能比 defer 更早执行。
3 次结果

defer 和 module 总是按照声明顺序执行。
4 次结果

结论:defer 和 module 不仅按照声明顺序执行。而且他们总是会在 DCL 和 interactive 俩事件中间执行。
即要等到 defer 和 module 脚本全部执行完毕后,才会触发浏览器 DOMContentLoaded 事件。于是你要注意你后续写代码的一些细节了。
应用价值
- SSR 同构的页面,由于存在初始 dom 内容,请把所有初始页面所需要的 js 弄成 defer,让他在 dom 解析完毕后执行。从而更快让浏览器渲染页面内容,晚点再执行你的激活和 vue 托管代码。 
 关于这一点,至于你是放到 body 末尾还是把 js 写在前面加 defer,我感觉时机和功能都差不多。
- 假设你的应用只需要兼容现代浏览器—即你使用了 modern 现代化的 module 语法来放到生产环境,那么 module 本身就是 defer 的。所以你无需做特殊处理。 
- 假设你页面中还需要引入一个百度统计、谷歌统计等第三方的脚本,或者公司内不太重要的一些数据采集脚本—即你的页面主逻辑并不依赖这个脚本的话。 那么你可以采用 async 声明该脚本。 
补充
有人问,在前端性能优化场景下,有时候一个页面除了初始化 js,还依赖一个“初始路由动态懒加载的 chunk”,这种要怎么办?
答:假设页面要用 app.js,且 app.js 里面又依赖了需要异步加载的 home.js 才能完整渲染当前页面(这在典型的 vue 单页项目中很常见)。那么对于这种初始页面渲染可能依赖一个未来要下载的资源的情况(例如 home.js 甚至是某些重要字体),我们要搬出我们的 ‘preload’声明技术了。他可以帮我们让浏览器尽快把这个资源下载下来,而不是等到逻辑运行到的时候再下载。