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’声明技术了。他可以帮我们让浏览器尽快把这个资源下载下来,而不是等到逻辑运行到的时候再下载。