分析网页资源如何阻塞浏览器加载

分析网页资源如何阻塞浏览器加载

TL; DR

  • 图片/视频/字体不会阻塞页面加载
  • CSS 会阻塞 DOM 渲染,但不会阻塞 DOM 解析
  • JS 的加载、解析、执行会阻塞 DOM 的解析
  • CSS 会阻塞 JS 执行,从而 JS 阻塞DOM 解析
  • DOMContentLoaded 是在 DOM 完全加载以及解析,并且所有defer-script (即<script defer src="…"> and <script type="module">) 下载并执行后触发
  • onload 是当页面所有资源(包括 CSSJS、图片、字体、视频等)都加载完成才触发
  • defer 是“渲染完执行”, async是“下载完执行”

问题 1:图片会造成阻塞吗?

来看以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script>
document.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded")
})
window.onload = function () {
console.log("onload")
}
</script>
</head>
<body>
<h1>我是h1标签</h1>
<img
src="https://cdn.jsdelivr.net/gh/L1atte/PicGo/img/蝴蝶忍4.jpg"
alt=""
/>
<h2>我是h2标签</h2>
</body>
</html>

在 slow 的 network 环境下,打开该页面,可以看到如下结果:

h1h2 标签渲染完成并且控制台打印了 DOMContentLoaded 的时候,图片还在加载中。这说明了图片不会阻塞 DOM 的加载,更不会阻塞页面渲染;当图片加载完成后,控制台打印 onload,这说明图片延迟了 onload 事件的触发

视频、字体和图片其实是一样的,也不会阻塞 DOM 的加载和渲染。

问题 2:CSS 加载会阻塞渲染吗

同样模拟慢速网络环境,来看以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css"
rel="stylesheet"
/>
</head>
<body>
<h1>我是h1标签</h1>
</body>
</html>

可以看到如下结果:

  1. bootstrap.css 还没加载完成的时候,页面出于空白状态,但是 DOM 中出现 h1 标签,说明 CSS 不会阻塞 DOM 解析
  2. 页面直到 bootstrap.css 加载完成才出现 h1 的内容,说明 CSS 会阻塞 DOM 的渲染

为什么是这样呢。联想一下网页渲染的过程就知道了。浏览器首先解析 html 生成 DOM 树,解析 CSS 生成 CSSOM 树,然后将 DOM 树和 CSSOM 树进行合成生成渲染树,通过渲染树进行 style(样式计算)、layout(布局)、paint(绘制),最终把页面渲染出来

网页渲染过程

所以说解析 DOM 和解析 CSS 是并行执行的,既然如何,他们的解析过程就不会相互影响了,这和结论一相符;同时渲染页面实在得到 CSSOM 树之后进行的,这和结论二相符

问题 3:CSS 会阻塞 JS 执行吗

模拟慢速网络环境,来看以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css"
rel="stylesheet"
/>
</head>
<body>
<h1>我是h1标签</h1>
<script>
console.log("script")
</script>
</body>
</html>

刷新浏览器的时候可以发现,在 bootstrap.css文件加载完之后控制台打印 script说明 CSS 会阻塞 JS 的执行

JS 会阻塞渲染吗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<h1>我是h1标签</h1>
<script>
document.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded")
})

const stopTime = Date.now() + 5000
while (Date.now() < stopTime) {}
</script>
</body>
</html>

当我们刷新浏览器的时候,页面一直没有加载出 h1 标签(白屏状态),直到 5s 后,显示 h1 标签,DOM 才渲染完成,同时打印 DOMContentLoaded这说明 JS 的执行会阻塞 DOM 的解析

我们再来验证下 JS 的加载是否会阻塞渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>
</head>
<body>
<h1>我是h1标签</h1>
<script>
document.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded")
})
</script>
</body>
</html>

刷新页面的时候,可以看到在 vue.js 文件下载完成后,显示 h1 标签和打印 DOMContentLoaded这说明 JS 的加载会阻塞 DOM 的解析

可以得出结论,浏览器解析 html 遇到 script 标签的时候会发生:

  • 暂停解析 DOM
  • 执行 script 里的脚本,如果该 script 是外链,则会先下载它,下载完成后立刻执行
  • 执行完成后继续解析剩余 DOM

defer 和 async 的作用,有什么区别?

上面我们提到 JS 的加载、执行都会阻塞页面的渲染,那么有什么方法可以解决这个问题呢。

MDN 上对于这两个属性的解释

defer: 这个布尔属性被设定用来通知浏览器该脚本将在文档完成解析后,触发 DOMContentLoaded 事件前执行。 有 defer 属性的脚本会阻止 DOMContentLoaded 事件,直到脚本被加载并且解析完成。

async: 对于普通脚本,如果存在 async 属性,那么普通脚本会被并行请求,并尽快解析和执行。该属性能够消除解析阻塞的 Javascript。解析阻塞的 Javascript 会导致浏览器必须加载并且执行脚本,之后才能继续解析。

也就是说,当我们通过 defer 或者 async 的方式加载 JS 的时候,他是不会阻塞 DOM 解析的

那么,他们两者的区别是什么呢,来看一张经典的图

defer 特点

  • 对于 deferscript,浏览器会继续解析 html,且同时下载脚本。等待 html Parser(即 DOM 构建)后开始执行
  • defer-script 加载的 JS 代码 执行完成后触发 DOMContentLoaded 事件
  • 多个 defer 的脚本执行顺序严格按照定义顺序进行,而不是先下载好的先执行

async 特点

  • 对于 asyncscript,浏览器会继续解析 html,并同时下载脚本,一旦脚本下载完成就会立刻执行。和 defer 一样,它的下载过程也是不会造成阻塞的,但是如果下载完成后 DOM 还没解析完成,那么执行脚本的时候也是会造成阻塞的
  • async 脚本的执行 和 DOMContentLoaded 的触发顺序无法明确谁先谁后,因为脚本可能在 DOM 构建完成时还没下载完,也可能早就下载好了
  • 多个 async,按照谁先下载完成谁先执行的原则进行,所以当它们之间有顺序依赖的时候特别容易出错

DOMContentLoaded 和 onload

在浏览器加载资源过程中涉及到两个事件,分别是 DOMContentLoadedonload ,那么它们有什么区别呢

DOMContentLoaded: The DOMContentLoaded event fires when the HTML document has been completely parsed, and all deferred scripts (<script defer src="…"> and <script type="module">) have downloaded and executed. It doesn’t wait for other things like images, subframes, and async scripts to finish loading.

参考:https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event

onload:当页面所有资源(包括 CSSJS、图片、字体、视频等)都加载完成才触发,而且它是绑定到 window 对象上
参考:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/load_event

DOMContentLoaded 遇上 JS

当浏览器处理一个 HTML 文档,并在文档中遇到 <script> 标签时,就会在继续构建 DOM 之前运行它。这是一种防范措施,因为脚本可能想要修改 DOM,甚至对其执行 document.write 操作,所以 DOMContentLoaded 必须等待脚本执行结束后才触发。以下这段代码验证了这个结论:当脚本加载完成的时候,Console 面板下才会打印出 DOMContentLoaded

1
2
3
4
5
6
7
<script>
document.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded")
})
</script>
<h1>我是 h1 标签</h1>
<script src="https://cdn.bootcss.com/jquery/2.1.4/jquery.min.js"></script>

那么一定是脚本执行完成后才会触发 DOMContentLoaded 嘛?答案也是否定的,有两个例外,对于 async 脚本是不会阻塞 DOMContentLoaded 触发的

DOMContentLoaded 遇到 CSS

前面我们已经介绍到 CSS 是不会阻塞 DOM 的解析的,所以理论上 DOMContentLoaded 应该不会等到外部样式的加载完成后才触发,这么分析是对的,让我们用下面代码进行测试一翻就知道了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
document.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded")
})
</script>
<link
href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css"
rel="stylesheet"
/>
</head>
<body>
<h1>我是 h1 标签</h1>
</body>
</html>

测试结果:当样式还没加载完成的时候,就已经打印出 DOMContentLoaded,这和我们分析的结果是一致的。

但是一定是这样嘛?显然不一定,这里有个小坑

(基于上面代码)在样式后面再加上 <script> 标签的时候,会发现只有等样式加载完成了才会打印出 DOMContentLoaded,为什么会这样呢?正是因为 <script> 会阻塞 DOMContentLoaded 的触发

所以当外部样式后面有脚本(async 脚本和动态脚本除外)的时候,外部样式就会阻塞 DOMContentLoaded 的触发

1
2
3
4
<!-- 在上面代码基础上添加 只显示了部分内容 -->
<link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="stylesheet"/>
<script></script>
</head>

DOMContentLoaded 的执行时机到底在什么时候

其实还是归根到定义上,**DOMContentLoaded 是在 DOM 完全加载以及解析,并且所有defer-script (即<script defer src="…"> and <script type="module">) 下载并执行后触发**

联系我们前面的结论:

  1. CSS 不会阻塞 DOM 解析
  2. JS 的加载、解析、执行会阻塞 DOM 解析
  3. CSS 会阻塞 JS 执行,从而阻塞 DOM 解析

所以,现在是不是一目了然啦!

参考文章