script脚本阻塞的探究、异步属性async和defer的区别

本文主要讨论html中script标签的一些特性,其中,script的阻塞是讨论的核心点,主要围绕script的阻塞探讨其表现及解决方式。

一、阻塞dom的解析渲染

1、描述

  • 由于js的执行可能会有操作dom的情况导致dom结构发生变化,而浏览器做了最坏的打算,所以在js执行时会暂停dom的解析渲染,这就造成了对dom的阻塞。
  • 记住这句话,任何情况下js的执行都会阻塞dom的渲染

2、常规解决方式

一般就是把js放在body标签的最底部,这样既保证dom解析完后才执行js,也保证js里能获取到dom元素。
另外不建议把script代码放在body闭合标签(</body>)的下面,某些特殊的webview容器可能无法识别就把script给丢弃了(例如阿里百川某个版本的webview)

二、阻塞后续script的执行

1、js报错异常造成的影响

  • 大家都知道,js是单线程的,一旦js代码报错就会造成阻塞,导致后续js代码不再执行。
  • 这里抛出个问题:如果有两个script标签,前一个script里的js报错会不会阻塞后面的script代码?
  • 实践一下呗,写个demo:
<html>
  <body>
    <div>Neo</div>

    <script>
      console.log(a) // 打印一个未定义的变量a
      console.log(0)
    </script>

    <script>
      console.log(1)
    </script>
  </body>
</html>
  • 运行结果如下,第一个script里打印a报错,导致打印0的语句未执行,但后面的script并未受到影响,正常打印出了1。:
    在这里插入图片描述
  • 结论:script里的代码异常只会阻塞同一script里后续的代码,不会影响其他script的代码。
  • 那是否意味着我的script怎么写怎么浪都没关系了?并不是,往下看。

2、js下载异常造成的影响

  • script引入js有两种方式,内联方式和外联方式,内联方式就是script标签包裹js代码,外联方式就是script标签通过src属性指定js的地址。
  • 内联方式里你script怎么写怎么浪都行,但外联方式就未必。
  • 你可以试着运行下面的代码试试:
<html>
  <body>
    <div>Neo</div>

    <script src="file://www.rogue.com/rogue.js"></script>

    <script>
      console.log(1)
    </script>
  </body>
</html>
  • 运行结果,html加载状态那里一直在转圈圈,起初没有任何打印输出,大概等了20秒后输出了如下结果:
    开始时:开始时
    20秒后:20秒后
  • 如上,浏览器查找"file://www.rogue.com/rogue.js"这个js文件,但是一直搜寻不到,导致后续的script也处于阻塞当中,这个问题和file协议没有关系,我这里只是找了个能作为示例的地址,http和https协议的地址都有可能发生。
  • 联想到实际项目中,通过script外联方式引入跨域的第三方插件时可能会有阻塞后续script代码的风险,一般是引用的跨域地址服务器不稳定或连接缓慢时发生,所以使用任何第三方的域名地址都要警惕,不管是开源组织的还是付费商用的,要把命运握在自己手里才踏实。

三、异步script

在不加任何异步属性的情况下,script的下载和执行都会阻塞dom的渲染,而添加异步属性可以使下载阶段异步进行。
以下是几种异步script的解释和对比:

1、async属性

  • 异步下载script代码。
  • 不支持内联方式,也就是script标签必须有src属性。
  • 执行时机:下载完后,立即执行。
  • 执行顺序:下载完js文件的顺序,即网络请求返回顺序,无法提前预知。
  • 使用示例:
<script async src="https://aaa.com"></script>
  • tips:这个async属性正好解决了上述讲的的“js下载异常造成的影响”。

2、defer属性

  • 异步下载script代码。(同async)
  • 不支持内联方式,也就是script标签必须有src属性。(同async)
  • 执行时机:下载完后,在dom解析完之后、触发DOMContentLoaded之前执行。(不同于async)
  • 执行顺序:如果带defer的script有多个,那它们将按照在页面中出现的顺序来依次执行。(不同于async)
  • 使用示例:
<script defer src="https://aaa.com"></script>

3、动态创建

  • 通过src属性赋值动态创建的script,未指定async属性时也是默认异步的。例如:
// 默认异步
var script = document.createElement('script')
script.src = "file.js"
document.body.appendChild(script) 
  • 通过innerHtml或eval方式创建的script,未指定async属性时默认不是异步。例如:
// 非异步
document.body.innerHTML = '<script src="file.js"></script>'
  • 关于以上动态创建的异步是属于哪种异步,由于没有合适的方式验证,在mdn上这块也没有说明,不过是在介绍async时说到的,所以猜想应该是async异步方式吧。
  • 为了防止不必要的麻烦,即使动态创建的script默认是异步,也建议在创建异步script时手动添加相应的异步属性,async属性值用truedefer属性值就用defer

4、用哪个

对于async和defer用哪个,看完上述两者的说明对比一下,应该能总结出一二了。

借来mdn上的建议总结

  • 如果脚本无需等待页面解析,且无依赖独立运行,那么应使用 async。
  • 如果脚本需要等待页面解析,且依赖于其它脚本,调用这些脚本时应使用 defer,将关联的脚本按所需顺序置于 HTML 中。

注意:

  • 文章开始时讲到任何情况下js的执行都会阻塞dom的渲染。同样的,async和defer也一样,它们只是使js的下载阶段异步,执行阶段仍然会阻塞dom。

四、思考

  • 现在的spa框架开发,大多数情况下我们不需要关注script标签的引入,脚手架和webpack都已经帮我们自动处理好了。
  • 而我探究这个问题的初衷是解决script方式引入第三方插件时的问题。
  • 像一些第三方插件例如微信jsbridge、埋点、监控、各种sdk什么的,很多都是需要通过script标签的方式来引入到html模板中,而如何引入才能做到对原项目的影响最小是个值得深思的问题。
  • 对于这类script的引入,个人建议:
    • 能放在body最底部的就放在body最底部。
    • 能加异步属性的就加异步属性,根据具体情况选择一种,没有特殊要求时优先async,其次defer。(因为webpack打包后的script也会放在body最底部,比你引入的第三方插件script还靠后,所以你仍然需要异步属性。)
    • 如果只能使用同步方式,要格外注意src地址的连接速度和稳定性,最好能把代码拉下来放在本地项目或自己的服务器域名下,非插件官方的地址不要轻易使用,特别是国外的域名地址。

参考链接:
[1] https://developers.google.com/web/fundamentals/performance/critical-rendering-path/adding-interactivity-with-javascript?hl=en
[2] https://zhuanlan.zhihu.com/p/292953374
[3] https://html.spec.whatwg.org/multipage/scripting.html#the-script-element

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值