本文主要讨论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秒后: - 如上,浏览器查找"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
属性值用true
,defer
属性值就用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