Open Bulandent opened 3 years ago
一个页面允许加载的外部资源有很多,常见的有脚本、样式、字体、图片和视频等,对于这些外部资源究竟是如何影响整个页面的加载和渲染的呢?今天我们来一探究竟。
阅读完这篇文章你将解开如下谜团:
测试之前我们需要对浏览器下载资源的速度进行控制,将它重新设置为 50kb/s,操作方式:
Chrome
Network
Disable cache
为什么是这个速度?因为如下的一些资源,比如图片、样式或者脚本体积都是 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,打开该页面,可以看到如下结果:当 h1 和 h2 标签渲染出来且打印了 DOMContentLoaded 的时候,此时图片还在加载中,这就说明了图片并不会阻塞 DOM 的加载,更加不会阻塞页面渲染;当图片加载完成的时候,会打印 onload,说明图片延迟了 onload 事件的触发。
h1
h2
DOMContentLoaded
DOM
onload
视频、字体和图片其实是一样的,也不会阻塞 DOM 的加载和渲染。
同样的,我们还是直接用代码来测试 CSS 加载对页面阻塞的情况,因为下面代码加载的 bootstrap.css 是 192kb 的,所以理论上下载它应该需要花费 3 到 4 秒左右。
CSS
bootstrap.css
<!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>
测试过程如下:
Elements
delete
我是 h1 标签
从而得出结论:
为什么是这个结论呢?试想一下页面渲染的流程就知道了。浏览器首先解析 HTML 生成 DOM 树,解析 CSS 生成 CSSOM 树,然后 DOM 树和 CSSOM 树进行合成生成渲染树,通过渲染树进行布局并且计算每个节点信息,绘制页面。
HTML
CSSOM
可以说解析 DOM 和 解析 CSS 其实是并列进行的,既然是并列进行的,那 CSS 和 DOM 就不会互相影响了,这和结论一相符;另外渲染页面一定是在得到 CSSOM 树之后进行的,这和结论二相符。
CSS 一定会阻塞 DOM 的渲染嘛?答案是否定的,当把外链样式放到 <body> 最尾部去加载:
<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> 里加载的原因。
<head>
CSS 阻塞了后面 DOM 的渲染,那它会阻塞 JS 的执行嘛?
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 的执行。
Console
为什么会这样呢?试想一下,如果 JS 里执行的操作需要获取当前 h1 标签的样式,而由于样式没加载完成,所以就无法得到想要的结果,从而证明了 CSS 需要阻塞定义在其之后 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> 的最尾部去加载,减少页面加载白屏时间。
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,也会得到这个结论。
async
defer
现在我们知道了通过 defer 或者 async 方式加载 JS 的时候,它是不会阻塞 DOM 加载的。那么你知道 defer 和 async 是什么嘛?它们两者有什么区别呢?
回答这些疑问之前,我们先来看下当浏览器解析 HTML 遇到 script 标签的时候会发生什么?
script
上面这是解析时遇到一个正常的外链的情况,正常外链的下载和执行都会阻塞页面解析;而如果外链是通过 defer 或者 async 加载的时候又会是如何呢?
defer 特点
html
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 一样。
script.async = false
在浏览器中加载资源涉及到 2 个事件,分别是 DOMContentLoaded 和 onload,那么它们之间有什么区别呢?
window
document
细心的你一定看到了上面的可能二字,为什么当 DOMContentLoaded 触发的时候样式和脚本是可能还没加载完成呢?
当浏览器处理一个 HTML 文档,并在文档中遇到 <script> 标签时,就会在继续构建 DOM 之前运行它。这是一种防范措施,因为脚本可能想要修改 DOM,甚至对其执行 document.write 操作,所以 DOMContentLoaded 必须等待脚本执行结束后才触发。以下这段代码验证了这个结论:当脚本加载完成的时候,Console 面板下才会打印出 DOMContentLoaded。
<script>
document.write
<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 触发的。
前面我们已经介绍到 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>
一个页面允许加载的外部资源有很多,常见的有脚本、样式、字体、图片和视频等,对于这些外部资源究竟是如何影响整个页面的加载和渲染的呢?今天我们来一探究竟。
阅读完这篇文章你将解开如下谜团:
测试前环境准备
测试之前我们需要对浏览器下载资源的速度进行控制,将它重新设置为 50kb/s,操作方式:
Chrome
开发者工具;Network
面板下找到Disable cache
右侧的下拉列表,然后选择 Add 添加自定义节流配置;为什么是这个速度?因为如下的一些资源,比如图片、样式或者脚本体积都是 50kb 的好几倍,方便测试。
图片会造成阻塞嘛
直接写个示例来看下结果:
上面这张图片的大小大概是 200kb,当把网络下载速度限制成 50kb/s,打开该页面,可以看到如下结果:当
h1
和h2
标签渲染出来且打印了DOMContentLoaded
的时候,此时图片还在加载中,这就说明了图片并不会阻塞DOM
的加载,更加不会阻塞页面渲染;当图片加载完成的时候,会打印onload
,说明图片延迟了onload
事件的触发。视频、字体和图片其实是一样的,也不会阻塞
DOM
的加载和渲染。CSS 加载阻塞
同样的,我们还是直接用代码来测试
CSS
加载对页面阻塞的情况,因为下面代码加载的bootstrap.css
是 192kb 的,所以理论上下载它应该需要花费 3 到 4 秒左右。测试过程如下:
Elements
面板下,选中h1
这个标签,然后按delete
键将它从DOM
中删掉,从而模拟首次加载;Elements
面板下就加载出h1
标签,继续加载 3 到 4 秒后(此时正在加载bootstrap.css
),页面出现我是 h1 标签
字样,此时页面已经渲染完成。从而得出结论:
bootstrap.css
还没加载完成,而DOM
中就已经出现h1
标签,说明CSS
不会阻塞DOM
的解析;bootstrap.css
加载完成才出现h1
里的文案,说明CSS
会阻塞DOM
的渲染。为什么是这个结论呢?试想一下页面渲染的流程就知道了。浏览器首先解析
HTML
生成DOM
树,解析CSS
生成CSSOM
树,然后DOM
树和CSSOM
树进行合成生成渲染树,通过渲染树进行布局并且计算每个节点信息,绘制页面。可以说解析
DOM
和 解析CSS
其实是并列进行的,既然是并列进行的,那CSS
和DOM
就不会互相影响了,这和结论一相符;另外渲染页面一定是在得到CSSOM
树之后进行的,这和结论二相符。CSS
一定会阻塞DOM
的渲染嘛?答案是否定的,当把外链样式放到<body>
最尾部去加载:此时刷新浏览器,页面上会马上显示出
我是 h1 标签
字样,当 3 到 4 秒过后样式加载完成的时会造成二次渲染,页面重新渲染出该字样,这就说明CSS
阻塞DOM
的渲染只阻塞定义在CSS
后面的DOM
。二次渲染会对用户造成不好的体验且加重了浏览器的负担,所以这也就是为什么需要把外链样式提前到<head>
里加载的原因。CSS 会阻塞后面 JS 的执行嘛
CSS
阻塞了后面DOM
的渲染,那它会阻塞JS
的执行嘛?刷新浏览器的时候可以看到,浏览器
Console
面板下没有打印内容,而当样式加载完成的时候打印了 888,这就说明CSS
会阻塞定义在其之后JS
的执行。为什么会这样呢?试想一下,如果
JS
里执行的操作需要获取当前h1
标签的样式,而由于样式没加载完成,所以就无法得到想要的结果,从而证明了CSS
需要阻塞定义在其之后JS
的执行。JS 加载阻塞
CSS
会阻塞DOM
的渲染和阻塞定义在其之后的JS
的执行,那JS
加载会对渲染过程造成什么影响呢?首先删除页面中已经存在的
h1
标签(如果存在的话),仔细观察Elements
面板,当刷新浏览器的时候,一直未加载出h1
标签(期间页面一直白屏),直到JS
加载完成后,DOM
中才出现,这足以说明了JS
会阻塞定义在其之后的DOM
的加载,所以应该将外部JS
放到<body>
的最尾部去加载,减少页面加载白屏时间。defer 和 async
JS
一定会阻塞定义在其之后的DOM
的加载嘛?来测试一下:上面这段代码的测试结果是当页面中显示出 h1 标签的时候,脚本还没有加载完成,这就说明了
async
脚本不会阻塞DOM
的加载;同理我们可以用同样的方式测试defer
,也会得到这个结论。现在我们知道了通过
defer
或者async
方式加载JS
的时候,它是不会阻塞DOM
加载的。那么你知道defer
和async
是什么嘛?它们两者有什么区别呢?回答这些疑问之前,我们先来看下当浏览器解析
HTML
遇到script
标签的时候会发生什么?DOM
;script
里的脚本,如果该script
是外链,则会先下载它,下载完成后立刻执行;DOM
。上面这是解析时遇到一个正常的外链的情况,正常外链的下载和执行都会阻塞页面解析;而如果外链是通过
defer
或者async
加载的时候又会是如何呢?defer
特点defer
的script
,浏览器会继续解析html
,且同时并行下载脚本,等DOM
构建完成后,才会开始执行脚本,所以它不会造成阻塞;defer
脚本下载完成后,执行时间一定是DOMContentLoaded
事件触发之前执行;defer
的脚本执行顺序严格按照定义顺序进行,而不是先下载好的先执行;async
特点async
的script
,浏览器会继续解析html
,且同时并行下载脚本,一旦脚本下载完成会立刻执行;和defer
一样,它在下载的时候也不会造成阻塞,但是如果它下载完成后DOM
还没解析完成,则执行脚本的时候是会阻塞解析的;async
脚本的执行 和DOMContentLoaded
的触发顺序无法明确谁先谁后,因为脚本可能在DOM
构建完成时还没下载完,也可能早就下载好了;async
,按照谁先下载完成谁先执行的原则进行,所以当它们之间有顺序依赖的时候特别容易出错。动态脚本会造成阻塞嘛
对于如下这段代码,当刷新浏览器的时候会发现页面上马上显示出
我是 h1 标签
,而过几秒后才加载完动态插入的脚本,所以可以得出结论:动态插入的脚本不会阻塞页面解析。动态插入的脚本在加载完成后会立即执行,这和
async
一致,所以如果需要保证多个插入的动态脚本的执行顺序,则可以设置script.async = false
,此时动态脚本的执行顺序将按照插入顺序执行和defer
一样。DOMContentLoaded 和 onload
在浏览器中加载资源涉及到 2 个事件,分别是
DOMContentLoaded
和onload
,那么它们之间有什么区别呢?onload
:当页面所有资源(包括CSS
、JS
、图片、字体、视频等)都加载完成才触发,而且它是绑定到window
对象上;DOMContentLoaded
:当 HTML 已经完成解析,并且构建出了DOM
,但此时外部资源比如样式和脚本可能还没加载完成,并且该事件需要绑定到document
对象上;细心的你一定看到了上面的可能二字,为什么当
DOMContentLoaded
触发的时候样式和脚本是可能还没加载完成呢?DOMContentLoaded 遇到脚本
当浏览器处理一个
HTML
文档,并在文档中遇到<script>
标签时,就会在继续构建DOM
之前运行它。这是一种防范措施,因为脚本可能想要修改DOM
,甚至对其执行document.write
操作,所以DOMContentLoaded
必须等待脚本执行结束后才触发。以下这段代码验证了这个结论:当脚本加载完成的时候,Console
面板下才会打印出DOMContentLoaded
。那么一定是脚本执行完成后才会触发
DOMContentLoaded
嘛?答案也是否定的,有两个例外,对于async
脚本和动态脚本是不会阻塞DOMContentLoaded
触发的。DOMContentLoaded 遇到样式
前面我们已经介绍到
CSS
是不会阻塞DOM
的解析的,所以理论上DOMContentLoaded
应该不会等到外部样式的加载完成后才触发,这么分析是对的,让我们用下面代码进行测试一翻就知道了:测试结果:当样式还没加载完成的时候,就已经打印出
DOMContentLoaded
,这和我们分析的结果是一致的。但是一定是这样嘛?显然不一定,这里有个小坑,(基于上面代码)在样式后面再加上<script>
标签的时候,会发现只有等样式加载完成了才会打印出DOMContentLoaded
,为什么会这样呢?正是因为<script>
会阻塞DOMContentLoaded
的触发,所以当外部样式后面有脚本(async
脚本和动态脚本除外)的时候,外部样式就会阻塞DOMContentLoaded
的触发。参考文章