异步渲染的下载和阻塞行为
在CSS/JS对DOM渲染的影响一文 探讨了静态页面中的JavaScript/CSS的载入和解析对DOM渲染的影响。 本文接着讨论异步渲染场景下JavaScript/CSS对DOM解析(Parsing)和渲染(Rendering)的影响。
TL;DR
- 动态插入的外部样式表或脚本不阻塞DOM解析或渲染。
- 动态插入的内联样式表或脚本会阻塞DOM解析和渲染。
- 未连接到DOM树的样式表或脚本(外部或内联)不会被下载、解析或执行。
- 可以通过
onload
和onerror
监听HTML资源标签载入结果,兼容IE需要onreadystatechange
。
外部样式表
动态插入的外联样式表不阻塞DOM渲染,当然也不阻塞解析。 我们可以通过插入一个样式表、再插入一些脚本和文字来测试:
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://cdn.jsdelivr.net/npm/animate.css@3.5.2/animate.css';
var script = document.createElement('script');
script.text = 'console.log("after link[rel=stylesheet]")';
var h2 = document.createElement('h2');
h2.innerHTML = 'Hello World';
document.body.innerHTML = '';
document.body.appendChild(link);
document.body.appendChild(script);
document.body.appendChild(h2);
在外部样式表仍在下载的过程中,后续的脚本("after link[rel=stylesheet]"
)已经执行,
文本也已经渲染(<h2>Hello World</h2>
):
内联样式表
与外链样式表不同,内联样式表会阻塞DOM解析(当然渲染也会被阻塞)。 其实不能叫阻塞啦,因为不涉及网络请求,内联样式表的解析本来就是同步的。
我们可以通过document.styleSheets
来检测样式表是否已经解析(Parse):
var style = document.createElement('style');
style.textContent = '*{ color: red }';
document.head.innerHTML = document.body.innerHTML = '';
console.log(document.styleSheets.length);
document.body.appendChild(style);
console.log(document.styleSheets[0].rules[0].cssText);
注意上述有两处console.log
。第一处是在<style>
尚未连接到DOM树时读取样式表的数目;
第二处是在插入<style>
标签后立即读取被解析的CSS规则:
- 插入前样式表为空,说明未连接到DOM树的内联样式不会被解析。
- 插入后样式表会被立即解析,甚至不会进入下一个事件循环。
外部脚本
动态插入的外部脚本的载入是异步的,不会阻塞解析或者渲染。 这意味着动态插入一个外部脚本后不可立即使用其内容,需要等待加载完毕。 例如:
var script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/react@15.4.0/dist/react.js';
document.body.appendChild(script);
console.log('after script', window.React);
我们会发现window.React
是空,等它加载结束后window.React
才可用:
内联脚本
与静态内联脚本一样,动态插入内联脚本也会阻塞DOM解析(Parsing)。
var script = document.createElement('script');
script.text = "console.log('from script');"
console.log('before script');
document.body.appendChild(script);
console.log('after script');
注意我们在插入脚本前后各输出一条记录,执行结果如下图:
"before script"
出现在"from script"
之前,说明未连接到DOM树的脚本不会被执行。"after script"
出现在"from script"
之后,说明内联脚本的插入会阻塞DOM解析。
未连接的CSS/JS不会被载入
通过上述实验我们知道没有连接到DOM树的内联CSS/JS不会被解析, 事实上没有连接到DOM树 (即browsing-context connected) 的外部CSS/JS也不会加载。
browsing-context connected
比连接到DOM树更加准确, 比如连接到了以当前DOM树中节点为根的ShadowDOM中也称browsing-context connected
。
也就是说如果你创建了一个<link rel="stylesheet">
(或<script>
)但并未连接到DOM树,那么它不会被加载。
这是标准行为与浏览器实现方式无关,因此你可以放心地利用该特性。
该特性很容易测试,只需创建一个<link rel="stylesheet">
(或<script>
)标签并查看是否产生网络请求:
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://cdn.jsdelivr.net/npm/animate.css@3.5.2/animate.css';
var script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/react@15.4.0/dist/react.js';
很显然,未连接到DOM树的<link rel="stylesheet">
(或<script>
)根本没有被下载:
如果将这两个资源标签连接到DOM树,你会立即看到Network记录:
document.body.append(link);
document.body.append(script);
Image
与CSS/JS的行为非常不一样,只要设置src
属性图片便会立即加载((new Image).src = 'foo'
)。 这一特性常被用来发送跨域访问日志。
资源载入事件
脚本和样式载入事件可以直接监听到,当然这只对非阻塞的资源获取有效。
需要注意的是浏览器兼容性:绝大多数情况监听onload
和onerror
即可,
为了支持IE浏览器,可以监听onreadystatechange
事件:
createScript('https://cdn.jsdelivr.net/npm/react@15.4.0/dist/react.js');
createScript('https://harttle.land/this/will/404.js');
function createScript(src){
var el = document.createElement('script');
el.src = src;
el.onload = () => console.log('load', el);
el.onerror = () => console.log('error', el);
el.onreadystatechange = () => console.log('readystatechange', el);
document.body.append(el);
}
对于这三个事件,在多数浏览器(包括Firefox和Chrome)下只会触发onload
和onerror
,
只有在IE下只会触发onreadystatechange
。请看:
嗯,在Chrome下触发了onload
和onerror
。
扩展阅读
- WHATWG Rendering: https://html.spec.whatwg.org/multipage/rendering.html
- WHATWG Stylesheet: https://html.spec.whatwg.org/multipage/semantics.html#link-type-stylesheet
本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可,转载注明来源即可: https://harttle.land/2016/11/26/dynamic-dom-render-blocking.html。如有疏漏、谬误、侵权请通过评论或 邮件 指出。