之前写了两篇文章,涉及到了页面访问整个过程的一些分析,比如页面生命周期的介绍,页面访问时渲染过程中 HTML、JS 的关系,前面两篇只是抓住了 JS,没有囊括 CSS,并且在复现上没有明确给出工具,而今天这篇文章将使用 Chrome 的 Network 和 Performance 工具去分析整个页面的加载过程
- Network: 分析请求,文章中用于分析请求发送时序关系
- Performance: 分析页面加载性能,用于查看 HTML, CSS, JS 解析时序
前排提示,使用上面两种工具去分析时,请打开无痕模式并且关掉在无痕模式允许运行的插件,目的是为了避免插件脚本的加载影响后期的分析
CSS
就目前类似的文章中,都是在头部导入采用的延时去模拟 CSS 可能会阻塞 DOM 解析的效果,对于 CSS 这类静态资源来说,它们是由专门的下载线程来下载的,不会阻塞 GUI 线程(HTML 解析所在线程),但是 <link>
和 <script>
是同步请求,就是需要发送请求后需要等待响应,那么阻塞 HTML 的真正原因可能是 1. 同步请求因为延迟导致的阻塞,等返回响应后 HTML 就会解析 2. HTML 遇到头部的外联 CSS 样式后,文件太大导致的阻塞
为了搞清楚这两个点,我分别设置了延时和大 CSS 文件两种模拟方式,代码如下
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="http://localhost:8000/500.css" />
</head>
<body>
<p id="test">究竟什么时候解析到我呢?</p>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="http://localhost:8000/big.css" />
</head>
<body>
<p id="test">究竟什么时候解析到我呢?</p>
</body>
</html>
延时模式下
大文件模式下
仔细看上面两个图蓝色虚线,也就是 DCL 所在的位置,全部都是在 CSS 加载开始一点点就完成了,对于上面的假设
- 同步请求阻塞
- 大文件阻塞
都没有影响到 HTML 的解析(注:外联样式 CSS 的请求实际上是在 Parse HTML 之前发生的)
由上面我们也可以知道,延时阻塞和大文件阻塞起到的效果是一致的,因此接下来的 demo 中都会采用延时的方案
由上我们可以知道,位于头部(头部一般指 <head>
中) CSS 不会阻塞 DOM 解析(头部是一个伏笔, 先不讲)
位于头部的 CSS 会阻塞页面渲染
页面加载的过程中实际上是一行行读取 HTML,因此页面的内容也是一点点的打印/渲染出来,所以就有了 First Plain 的出现,含义为页面从开始加载到页面内容的任何部分在屏幕上完成渲染的时间,以今日头条的访问为例,当页面还在转圈时,页面内容其实就已经能够被看到大部分了,之所以还在转圈是因为图片等资源还没有下载完成,而根据白屏时间的定义不就是 First Plain 之前的时间吗?
CSS 阻塞页面渲染,为了避免页面重复渲染(比如 红 -> 绿 -> 白 -> ...),页面会等待 CSS 的下载完成,前面测试了延时和大文件 CSS 的加载,两张图中红色部分为 load,绿色为 First Plain,即页面第一次渲染,由此可见在头部的 CSS 阻塞页面的渲染,不会阻塞 DOM
凡事有例外 - FOUC
当 <link>
被置于某些元素的中间,就会阻塞 dom 的解析了,看看下面的例子
<!DOCTYPE html>
<html>
<body>
<p>究竟什么时候解析到我呢?</p>
</body>
<!-- 延时 500ms 发送 CSS 文件 -->
<link rel="stylesheet" href="http://localhost:8000/500.css" />
</html>
结果如下
震惊!DOMContentLoaded 竟然在 Load 的后面(红线是 Load,蓝线是 DOMContentLoaded),至少在 Chrome 的 Performance 上给出了我们这样的答案
那么对于 HTML 的解析呢?看下图
HTML 解析的间隔时间出现了断层,也就是 CSS 的加载已经阻塞了 HTML 的解析,也就是 DOM 的解析,不同于在 <head>
标签的情况,DOMContentLoaded 需要等待 CSS 的完全加载,对于类似文章的观点,CSS 不会阻塞 DOM 的解析,在这个例子当中是错误的
也就是说,存在特例,<head>
尾部的 CSS 会阻塞位于其后紧跟的 DOM 的解析
这种情况被称为 FOUC(Flash of Unstyled Content), 样式闪烁, 因为 First Plain 机制的存在, 会将已经解析的 DOM 和 CSSOM 结合生成渲染树并进行部分渲染, 当该 CSS 加载完成后, 才会继续解析 DOM, 而且之前已经渲染的内容将会根据解析完成的 CSS 进行重绘
注意, FOUC 在不同的浏览器下可能会有不同的表现, Firefox, Chrome, Edge 表现一致, 如果要考虑其他浏览器其自行测试
CSS 部分 - 结论
页面加载 - 以展现内容的时机为基准
- CSS 放在头部加载 - 会阻塞页面加载, 页面内容会等待 CSS 的加载完成, 即使它需要加载 10s
- CSS 放在尾部加载 - 不会阻塞页面加载, 因为 First Plain 机制, CSS 前的已经被解析页面内容的会被渲染, 但是存在重绘
DOM 解析 - 以触发 DOMContentLoaded 的时机为基准
- CSS 放在头部加载 - 不会阻塞 DOM 解析
- CSS 放在尾部加载 - 会直接阻塞 DOM 解析
此时 CSS 特指外联样式表 <link rel="stylesheet" href="xxx">
JS 和 CSS
JS 的确阻塞 DOM 的解析,详细的内容可以看这篇文章,页面访问时渲染过程中 HTML、JS 的关系,那如果是 JS 和 CSS 一起呢?上面的分析给出了外联样式 CSS 放在头部和尾部是有不同的情况的,而没有 CSS 的情况下,把 JS 放在 HTML 任意部分都会阻塞 DOM 的解析,那么 CSS 混合 JS 就可以有以下 4 种情况
头部中引入外联样式
<head>...<link />...</head>
- JS 在 CSS 前
<head>...<script></script>...<link />...</head>
JS 在 CSS 后
<head>...<link />...<script></script>...</head>
<head>...<link />...</head>......<script></script>
- JS 在 CSS 前
尾部中引入外联样式
<body></body>...<link />
- JS 在 CSS 前
<body></body>...<link />...<script></script>
- JS 在 CSS 前
<body></body>...<script></script>...<link />
- JS 在 CSS 前
例子如下
<!-- 1 -->
<!DOCTYPE html>
<html>
<head>
<script>
console.log(document.querySelector("p"));
</script>
<link rel="stylesheet" href="http://localhost:8000/big.css" />
</head>
<body>
<p id="test">究竟什么时候解析到我呢?</p>
</body>
</html>
第一种情况,JS 会阻塞 DOM 的解析,先执行 <scirpt>
的内容,无需等待 CSS 加载
<!-- 2 -->
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="http://localhost:8000/big.css" />
<script>
console.log(document.querySelector("p"));
</script>
</head>
<body>
<p id="test">究竟什么时候解析到我呢?</p>
</body>
</html>
第二种情况,JS 需要等待 CSS 的加载完成才能够执行,因为 JS 可能需要获取到 CSS 中设置好的样式属性,比如宽度和高度等,那么后面的 DOM 就无法解析了,因此 CSS 加载 -> JS 等待 CSS 加载 -> JS 阻塞 DOM
注意,补充一个小细节,看看下面这个例子
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="http://localhost:8000/big.css" />
</head>
<body>
<p id="test">究竟什么时候解析到我呢?</p>
</body>
<script>
console.log(document.querySelector("p"));
</script>
</html>
同样是 CSS 在 JS 前,head 中的 JS 是获取不到 <p>
的,但 <body>
下面的是能获取到的,证明虽然 DOM 的解析被阻塞了,但是已经被解析出来的 DOM 是不影响操作的
<!-- 3 -->
<!DOCTYPE html>
<html>
<body>
<p id="test">究竟什么时候解析到我呢?</p>
</body>
<script>
console.log(document.querySelector("p"));
</script>
<link rel="stylesheet" href="http://localhost:8000/500.css" />
</html>
第三种情况,同第一种情况,无需等待 CSS 的加载,但是 DOMCOntentLoaded 需要等待 CSS 的加载
<!-- 4 -->
<!DOCTYPE html>
<html>
<body>
<p id="test">究竟什么时候解析到我呢?</p>
</body>
<link rel="stylesheet" href="http://localhost:8000/500.css" />
<script>
console.log(document.querySelector("p"));
</script>
</html>
第四种情况,同第二种情况,需等待 CSS 的加载,但是 DOMCOntentLoaded 需要等待 CSS 的加载
因此 DOMContentLoaded 在 MDN 上的定义在实际中并不一定准确, 或者说语义上有点误解, DOMContentLoaded 的触发会因为 JS 的位置决定是否需要等待 CSS 的加载
CSS 的加载在 JS 前则会延迟 DOMContentLoaded, 原因是 CSS 的加载将会阻塞 JS 的执行, 而 DOMContentLoaded 的触发需要等待 HTML 的内联 JS 和外联 JS 的加载执行完成, 因此 CSS 间接延迟了 DOMContentLoaded 的触发
在 Firefox, Chrome, Edge 中,如果 <link rel="stylesheet" href="xxx">
后面跟着 <script>
, 则 CSS 加载完成后, 才能触发 DOMContentLoaded
JS 和 CSS 部分结论
- CSS 阻塞 JS 执行
- CSS 间接阻塞 DOMContentLoaded, 因为 JS 需要等待 CSS 加载
- JS 会阻塞 DOMContentLoaded 触发时间, 但不影响其操作在其之前的 DOM
- 页面加载(以展现内容的时机为基准), JS 不会阻塞在其之前的 HTML 内容的渲染
总结
对于 JS ,在交互上,想要正确的操作 DOM,将 JS 放在 HTML 尾部,当解析到 JS 时,虽然没有触发 DOMCOntentLoaded,但是已经可以正确操作 DOM,减少加载时间,将 JS 异步化,对于外部 JS,采用添加 defer 属性或者 async 引入
而 CSS 在头部不会阻塞 DOM 的解析,但是跟在其后的 JS 在 CSS 加载的这段时间是不能够被执行的, 即使 JS 下载完成时间比 CSS 要快, 优先加载 CSS 避免 FOUC 的出现