4

探究网页资源究竟是如何阻塞浏览器加载的

 2 years ago
source link: https://bubuzou.com/2020/12/26/browser-block/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

探究网页资源究竟是如何阻塞浏览器加载的

2020-12-26

一个页面允许加载的外部资源有很多,常见的有脚本、样式、字体、图片和视频等,对于这些外部资源究竟是如何影响整个页面的加载和渲染的呢?今天我们来一探究竟。

阅读完这篇文章你将解开如下谜团:

  • 如何用 Chrome 定制网络加载速度?
  • 图片/视频/字体会阻塞页面加载嘛?
  • CSS 是如何阻塞页面加载的?
  • JS 又是如何阻塞页面加载的?
  • JS 一定会阻塞 DOM 加载嘛?
  • defer 和 async 是什么?又有何特点?
  • 动态脚本会造成阻塞嘛?
  • 阻塞是怎么和 DOMContentLoaded 与 onload 扯上关系的?

测试前环境准备

测试之前我们需要对浏览器下载资源的速度进行控制,将它重新设置为 50kb/s,操作方式:

  1. 打开 Chrome 开发者工具;
  2. Network 面板下找到 Disable cache 右侧的下拉列表,然后选择 Add 添加自定义节流配置;
  3. 添加一个下载速度为 50kb/s 的配置;
  4. 最后在第二步骤中的下拉列表选择刚刚配置的选项即可;
  5. 注意:如果当前选择的自定义选项被修改了,则需要切换到别的选项再切回来才可生效。

browser_005.png

为什么是这个速度?因为如下的一些资源,比如图片、样式或者脚本体积都是 50kb 的好几倍,方便测试。

图片会造成阻塞嘛

直接写个示例来看下结果:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<script>
document.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded')
})
window.onload = function() {
console.log('onload')
}
</script>
</head>
<body>
<h1>我是 h1 标签</h1>
<img src="https://xxx.oss-cn-shenzhen.aliyuncs.com/images/flow.png" />
<h2>我是 h2 标签</h2>
</body>
</html>

上面这张图片的大小大概是 200kb,当把网络下载速度限制成 50kb/s,打开该页面,可以看到如下结果:当 h1h2 标签渲染出来且打印了 DOMContentLoaded 的时候,此时图片还在加载中,这就说明了图片并不会阻塞 DOM 的加载,更加不会阻塞页面渲染;当图片加载完成的时候,会打印 onload,说明图片延迟了 onload 事件的触发。

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

CSS 加载阻塞

同样的,我们还是直接用代码来测试 CSS 加载对页面阻塞的情况,因为下面代码加载的 bootstrap.css 是 192kb 的,所以理论上下载它应该需要花费 3 到 4 秒左右。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<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. Elements 面板下,选中 h1 这个标签,然后按 delete 键将它从 DOM 中删掉,从而模拟首次加载;
  2. 刷新浏览器,马上 Elements 面板下就加载出 h1 标签,继续加载 3 到 4 秒后(此时正在加载 bootstrap.css),页面出现 我是 h1 标签 字样,此时页面已经渲染完成。

从而得出结论:

  • bootstrap.css 还没加载完成,而 DOM 中就已经出现 h1 标签,说明 CSS 不会阻塞 DOM 的解析
  • 页面直到 bootstrap.css 加载完成才出现 h1 里的文案,说明 CSS 会阻塞 DOM 的渲染

为什么是这个结论呢?试想一下页面渲染的流程就知道了。浏览器首先解析 HTML 生成 DOM 树,解析 CSS 生成 CSSOM 树,然后 DOM 树和 CSSOM 树进行合成生成渲染树,通过渲染树进行布局并且计算每个节点信息,绘制页面。

browser_006.png

可以说解析 DOM 和 解析 CSS 其实是并列进行的,既然是并列进行的,那 CSSDOM 就不会互相影响了,这和结论一相符;另外渲染页面一定是在得到 CSSOM 树之后进行的,这和结论二相符。

CSS 一定会阻塞 DOM 的渲染嘛?答案是否定的,当把外链样式放到 <body> 最尾部去加载:

<body>
<h1>我是 h1 标签</h1>
<link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="stylesheet" />
</body>

此时刷新浏览器,页面上会马上显示出 我是 h1 标签 字样,当 3 到 4 秒过后样式加载完成的时会造成二次渲染,页面重新渲染出该字样,这就**说明 CSS 阻塞 DOM 的渲染只阻塞定义在 CSS 后面的 DOM**。二次渲染会对用户造成不好的体验且加重了浏览器的负担,所以这也就是为什么需要把外链样式提前到 <head> 里加载的原因。

CSS 会阻塞后面 JS 的执行嘛

CSS 阻塞了后面 DOM 的渲染,那它会阻塞 JS 的执行嘛?

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<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('888')
</script>
</body>
</html>

刷新浏览器的时候可以看到,浏览器 Console 面板下没有打印内容,而当样式加载完成的时候打印了 888,这就说明 CSS 会阻塞定义在其之后 JS 的执行

为什么会这样呢?试想一下,如果 JS 里执行的操作需要获取当前 h1 标签的样式,而由于样式没加载完成,所以就无法得到想要的结果,从而证明了 CSS 需要阻塞定义在其之后 JS 的执行。

JS 加载阻塞

CSS 会阻塞 DOM 的渲染和阻塞定义在其之后的 JS 的执行,那 JS 加载会对渲染过程造成什么影响呢?

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<script src="https://cdn.bootcss.com/jquery/2.1.4/jquery.min.js"></script>
</head>
<body>
<h1>我是 h1 标签</h1>
</body>
</html>

首先删除页面中已经存在的 h1 标签(如果存在的话),仔细观察 Elements 面板,当刷新浏览器的时候,一直未加载出 h1 标签(期间页面一直白屏),直到 JS 加载完成后,DOM 中才出现,这足以说明了 JS 会阻塞定义在其之后的 DOM 的加载,所以应该将外部 JS 放到 <body> 的最尾部去加载,减少页面加载白屏时间。

defer 和 async

JS 一定会阻塞定义在其之后的 DOM 的加载嘛?来测试一下:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<script async src="https://cdn.bootcss.com/jquery/2.1.4/jquery.min.js"></script>
</head>
<body>
<h1>我是 h1 标签</h1>
</body>
</html>

上面这段代码的测试结果是当页面中显示出 h1 标签的时候,脚本还没有加载完成,这就说明了 async 脚本不会阻塞 DOM 的加载;同理我们可以用同样的方式测试 defer,也会得到这个结论。

现在我们知道了通过 defer 或者 async 方式加载 JS 的时候,它是不会阻塞 DOM 加载的。那么你知道 deferasync 是什么嘛?它们两者有什么区别呢?

回答这些疑问之前,我们先来看下当浏览器解析 HTML 遇到 script 标签的时候会发生什么?

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

上面这是解析时遇到一个正常的外链的情况,正常外链的下载和执行都会阻塞页面解析;而如果外链是通过 defer 或者 async 加载的时候又会是如何呢?

browser_009.png

defer 特点

  • 对于 deferscript,浏览器会继续解析 html,且同时并行下载脚本,等 DOM 构建完成后,才会开始执行脚本,所以它不会造成阻塞;
  • defer 脚本下载完成后,执行时间一定是 DOMContentLoaded 事件触发之前执行;
  • 多个 defer 的脚本执行顺序严格按照定义顺序进行,而不是先下载好的先执行;

async 特点

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

defer 和 async 都只能用于外部脚本,如果 script 没有 src 属性,则会忽略它们。

动态脚本会造成阻塞嘛

对于如下这段代码,当刷新浏览器的时候会发现页面上马上显示出 我是 h1 标签,而过几秒后才加载完动态插入的脚本,所以可以得出结论:动态插入的脚本不会阻塞页面解析

<!-- 省略了部分内容 -->
<script>
function loadScript(src) {
let script = document.createElement('script')
script.src = src
document.body.append(script)
}
loadScript('https://cdn.bootcss.com/jquery/2.1.4/jquery.min.js')
</script>
<h1>我是 h1 标签</h1>

动态插入的脚本在加载完成后会立即执行,这和 async 一致,所以如果需要保证多个插入的动态脚本的执行顺序,则可以设置 script.async = false,此时动态脚本的执行顺序将按照插入顺序执行和 defer 一样。

DOMContentLoaded 和 onload

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

  • onload:当页面所有资源(包括 CSSJS、图片、字体、视频等)都加载完成才触发,而且它是绑定到 window 对象上;
  • DOMContentLoaded:当 HTML 已经完成解析,并且构建出了 DOM,但此时外部资源比如样式和脚本可能还没加载完成,并且该事件需要绑定到 document 对象上;

细心的你一定看到了上面的可能二字,为什么当 DOMContentLoaded 触发的时候样式和脚本是可能还没加载完成呢?

DOMContentLoaded 遇到脚本

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

<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 是不会阻塞 DOM 的解析的,所以理论上 DOMContentLoaded 应该不会等到外部样式的加载完成后才触发,这么分析是对的,让我们用下面代码进行测试一翻就知道了:

<!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 的触发

<!-- 只显示了部分内容 -->
<link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="stylesheet"/>
<script></script>
</head>

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK