网页加载与阻塞
前言
注:本文写于2020年较早的时候,只是最近整理发布,因此关于渲染流程的认识比较肤浅,重点关注阻塞策略部分就好了;
基本流程
相关概念
-
DOM
:Document Object Model
,即文档对象模型;用于描述文档的结构及元素属性,是HTML和XML文档的编程接口(和JS
进行交互的接口)。 -
CSSOM
:CSS Object Model
,即CSS
对象模型;跟DOM
类似,不过CSSOM
是用来描述样式的结构和属性,是CSS
的编程接口(和JS
进行交互的接口)。 -
Render Tree
:是DOM
和CSSOM
两者的结构,渲染树即包含可以被渲染的元素节点,同时也包含对应节点的样式信息;哪些节点是不会被解析到渲染树中?
- 不包括
head
、script
、meta
等不可见的节点; - 某些通过
CSS
隐藏的节点在渲染树中也会被忽略,比如应用了display:none
规则的节点(visibility: hidden
只是视觉不可见,仍占据空间,不会被忽略)[1];
- 不包括
-
Layout
:布局;根据渲染树进一步计算出每个渲染节点的位置和尺寸信息(对应顶点着色器的工作?); -
Paint
:绘制;根据布局信息,得出渲染节点的像素信息,最终通过显卡绘制到屏幕的相应区域(对应片元着色器的工作?); -
回流:
reflow
,也叫重排;该流程会重新计算文档中元素的布局信息,某些对DOM
或CSS
的修改就会引发回流; -
重绘:
repaint
,即重新绘制部分或全部文档元素;当元素的像素信息改变时会触发重绘,很明显,回流必定引发重绘;
阻塞策略
虽然普通的link
及script
会阻塞DOM
的解析,但是有一些方法可以调整原始的阻塞策略;
调整script位置
由于script
不论内联还是外部,不论加载与否都会阻塞DOM
的解析,因此将script
标签移至DOM
最后(即<body>
闭合标签前),可以最小地影响DOM
的解析。
defer/async属性
这两个可以用于非内联script
标签(即必须设置src
属性),当设置为true
时,对应的script
标签不会阻塞DOM
的解析,但是两个属性的作用还是有所不同的。
defer属性
defer
,意为延迟;该属性除了可以使script
标签不阻塞DOM
解析,还会使对应的script
标签的执行阶段延迟到DOM
解析完成后,DOMContentLoaded
事件触发前;也就是所有的defer
脚本都必须在加载完且DOM
解析完成后才能执行,且按照顺序执行;
async属性
这个属性就比较简单粗暴,仅仅只是不阻塞DOM
而已,加载完后对应的script
会按顺序执行,因此执行的时机取决于加载完成的时间点(有可能在DOMContentLoaded
事件触发前执行,也可能在触发后执行),但是必定会在window
的load
事件触发前执行;
预加载策略
预加载,顾名思义就是预先加载某些会用到的文件,可以是脚本、样式,也可以是媒体文件等;通过预加载设置可以改变原本的一些文件加载顺序和优先级;
设置link
标签的rel
属性为preload
或prefetch
可以开启对应文件的预加载,此时href
属性值就是预加载的文件地址,然后通过as
属性可以指定文件用途(浏览器内部根据不同的as
类型会有不同的加载优先级);
preload 还有许多其他好处。使用 as来指定将要预加载的内容的类型,将使得浏览器能够:
- 更精确地优化资源加载优先级。
- 匹配未来的加载需求,在适当的情况下,重复利用同一资源。
- 为资源应用正确的内容安全策略。
- 为资源设置正确的 Accept 请求头。[2]
比如,可以通过设置对应的预加载,提前在头部加载脚本文件,然后在底部执行脚本;这样既能避免阻塞DOM
解析,又可以提前加载脚本文件;
1 | <head> |
preload和prefetch的区别
prefetch
虽然也是对资源文件进行预加载,但是其设计目的是为了预加载下一页或之后可能会跳转的页面所需要用到的文件,因此其优先级低于preload
。
验证实验
-
script
的执行是否阻塞DOM
的解析?1
2
3
4
5
6
7
8
9
10
11<p id="t1">t1</p>
<script>
for (let i = 1; i < 1000000000; i++) {
// 模拟同步执行耗时
}
console.log(1, window.t2)
</script>
<p id="t2">t2</p>
<script>
console.log(2, window.t2)
</script>根据上述代码设计,如果
script
的执行不会阻塞DOM
解析,那么第一处打印应该不会是undefined
;反之若阻塞DOM
解析,则为undefined
;实际结果为:因此答案为:
script
的执行会阻塞DOM
的解析; -
script
的加载是否阻塞DOM
的解析?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<head>
<script>
let start = Date.now()
</script>
<script>
setTimeout(() => {
console.log(1, window.t1)
}, 10)
</script>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.js"></script>
</head>
<body>
<p id="t1">t1</p>
<script>
console.log(Date.now() - start)
console.log(2, window.t1)
</script>
</body>根据上述设计,如果外部
script
的加载不会阻塞DOM
解析,那么第一处的打印将不会是undefined
;反之则为undefined
;测试的时候可以将网速调慢,这样对比更明显:得到的测试结果为:
因此答案为:
script
的加载会阻塞DOM
的解析; -
css
的加载是否阻塞DOM
的解析?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<head>
<script>
let start = Date.now()
</script>
<script>
setTimeout(() => {
console.log(1, window.t1)
}, 100)
</script>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.5.0/css/bootstrap-grid.css" rel="stylesheet">
</head>
<body>
<p id="t1">t1</p>
<script>
console.log(Date.now() - start)
console.log(2, window.t1)
</script>
</body>同理,首先降低网速,得到的打印结果为:
因此答案为:
css
的加载不会阻塞DOM
的解析; -
css
的加载是否阻塞JS
的执行?1
2
3
4
5
6
7
8
9
10
11
12
13<head>
<script>
let start = Date.now()
</script>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.5.0/css/bootstrap-grid.css" rel="stylesheet">
<script>
console.log(Date.now() - start)
console.log(1, window.t1)
</script>
</head>
<body>
<p id="t1">t1</p>
</body>同样地,降低网速进行测试,得到打印结果:
显然,
css
文件之后的js
是在css
文件加载之后才执行的;因此答案为:
css
的加载会阻塞JS
的执行; -
css
的加载是否阻塞DOM
的渲染?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<head>
<style>
#t1 {
color: red;
}
</style>
<script>
let start = Date.now()
console.log('start')
setTimeout(() => {
console.log(1, window.t1)
}, 10)
</script>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.5.0/css/bootstrap.css" rel="stylesheet">
</head>
<body>
<p id="t1">t1</p>
<script>
console.log(Date.now() - start)
</script>
</body>同样地,降低网速进行测试,录制结果为:
从录制
gif
可以明显看到虽然css
加载不阻塞DOM
的解析,但是加载过程中DOM
是不会被渲染到浏览器的,直到css
文件加载完成(CSSOM
构建完成)才会进行渲染,因此css
的加载会阻塞DOM
的渲染,造成白屏; -
设置
defer
的script
加载是否阻塞DOM
的解析?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<head>
<script>
let start = Date.now()
</script>
<script>
setTimeout(() => {
console.log(1, window.t1)
}, 0)
</script>
<script src="./test06.js" defer></script>
</head>
<body>
<p id="t1">t1</p>
<script>
document.addEventListener('DOMContentLoaded', () => {
console.log(Date.now() - start)
console.log('DOMContentLoaded', window.t1)
})
</script>
</body>1
2// test06.js
console.log('外部JS')同样地,降低网速进行测试,得到打印结果:
从打印结果不难看出加了
defer
属性的script
加载并不会阻塞DOM
的解析,而且会在DOMContentLoaded
事件之前执行完; -
设置
async
的script
加载是否阻塞DOM
的解析?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<head>
<script>
let start = Date.now()
</script>
<script>
setTimeout(() => {
console.log(1, window.t1)
}, 0)
</script>
<script src="./test07.js" async></script>
</head>
<body>
<p id="t1">t1</p>
<script>
document.addEventListener('DOMContentLoaded', () => {
console.log(Date.now() - start)
console.log('DOMContentLoaded', window.t1)
})
window.addEventListener('load', () => {
console.log('load')
})
</script>
</body>同样地,降低网速进行测试,得到打印结果:
结果跟
defer
类似,不会阻塞DOM
的解析,而且async
脚本并不会影响到DOMContentLoaded
事件的触发,但是会影响到load
事件的触发; -
进行预加载(
preload
)的script
是否阻塞DOM
的解析?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<head>
<script>
let start = Date.now()
</script>
<script>
setTimeout(() => {
console.log(1, window.t1)
}, 0)
</script>
<link href="./test08.js" rel="preload" as="script">
</head>
<body>
<p id="t1">t1</p>
<script src="./test08.js"></script>
</body>同样地,降低网速进行测试,得到打印结果:
结果很明显:预加载的
script
不会阻塞DOM
的解析;但是,如果预加载的
script
后面有其他DOM
,这时仍然会阻塞后面的DOM
解析!
上述实验源码放在这里:https://github.com/xxf1996/web-load-test
实验结论
根据以上实验测试,可以得出:
css
的加载会阻塞其后的js
执行,不会阻塞DOM
的解析,但是会阻塞DOM
的渲染;js
的加载和执行都会阻塞DOM
的解析;- 给
script
设置defer
或async
属性可以避免阻塞DOM
的解析;
相关文档
- 浏览器的渲染:过程与原理 - 知乎
- 页面渲染:过程分析 - 掘金
- 浏览器渲染页面过程与页面优化 - 前端小事 - SegmentFault 思否
- DOM概述 - Web API 接口参考 | MDN
- 初探CSS对象模型(CSSOM)_JavaScript, CSS, CSSOM, 会员专栏 教程_w3cplus
- Preloading content with rel=“preload” - HTML: Hypertext Markup Language | MDN
- DOMContentLoaded - 事件参考 | MDN
- css加载会造成阻塞吗? - 掘金
- Script标签和脚本执行顺序 - JavaScript编程语言