阻塞渲染分为两类:
CSS阻塞渲染
从浏览器系列 -- 渲染原理及过程中可得知第3步在构建渲染树时,需要完备的DOM树,CSSOM树,而CSSOM的解析可能会阻塞DOM解析,或者CSSOM树的未完成会阻塞渲染树的构建,这就是CSS阻塞渲染
JS阻塞渲染
JS 可以查询和修改 DOM 与 CSSOM,所以当 HTML 解析器遇到一个 script 标记时,它会暂停构建 DOM,将控制权移交给 JS 引擎, HTML 解析器会等待 JS 引擎运行完毕,这就是所谓的JS阻塞渲染
CSS阻塞渲染
1. 阻塞 DOM 解析(CSSOM 树的构建通过阻塞 JS 代码而阻塞 DOM 的解析)
<html>
<head>
<style type="text/css" src = "theme.css" />
</head>
<body>
<p>ALKAOUA</p>
<script>
let e = document.getElementsByTagName('p')[0]
e.style.color = 'blue'
</script>
<p>个人博客</p>
</body>
</html>
当在 JavaScript 中访问了某个元素的样式,那么这时候就需要 等待这个样式被下载 完成才能继续往下执行,所以在这种情况下(从某种角度看),CSS 也会阻塞 DOM 的解析
2. 阻塞渲染树的构建(CSSOM 树的构建会阻塞 render 树的构建)
<body>
<h1>red1</h1>
<link rel="stylesheet" href="https://www.youtube.com/file.css"><!--这里会卡一段时间-->
<h1>red2</h1>
</body>
- 可以看到页面依旧处于白屏状态
- 点击浏览器的停止加载按钮,red1内容会被渲染出来,此时查看Dom树,发现是没有
<h1>red2</h1>这个节点的 - 当资源下载失败时,
<h1>red2</h1>这个DOM节点才会被解析到,然后渲染出来
如何解决CSS阻塞渲染
1. CSS引入的位置 —— 针对阻塞 DOM 解析
一般我们把<style>、<link>放在<head>里面,提前加载好CSS资源,这样当 JavaScript 请求到样式表时将不必等待 CSS 资源的加载
2. 媒体查询的方式 —— 针对阻塞 render 树的构建
有些 CSS 资源在首次渲染中可能不用用到,只是在用户交互(比如改变页面大小)时才会用到,所以我们通过媒体查询的方式来判断是否需要在首次渲染加载。这样从某种程度上会减少首屏加载时间
<!-- 适用于所有情况,始终阻塞渲染 -->
<link href="style.css" rel="stylesheet">
<!-- 网页首次加载时,只在打印内容时适用 -->
<link href="print.css" rel="stylesheet" media="print">
<!-- 如果不是在打印内容时,该样式表不阻塞渲染 -->
<!-- 符合条件时浏览器将阻塞渲染,直至样式表下载并处理完毕 -->
<link href="other.css" rel="stylesheet" media="(max-width: 400px)">
<!-- 如果不满足条件,不会阻塞渲染,但依旧会请求下载对应的资源 -->
JS阻塞渲染
JS阻塞渲染与CSS阻塞渲染的最大区别在于css的解析是可预测的,而JS阻塞渲染是不可预测的,因为JS可能随时修改DOM节点,乃至动态加载
1. 内联脚本阻塞渲染
<body>
<h1>AAA</h1>
<script>
let d = Date.now()
while (Date.now() < d + 1000 * 3) { }
</script>
<h2>BBB</h2>
</body>
- 刚加载时
页面白屏,3秒后才会渲染出内容 - 说明内联JS会阻塞 DOM 解析和渲染,并且会一直阻塞
2. 外联同步脚本阻塞渲染
<body>
<h1>AAA</h1>
<script src="./test.js"></script>
<h2>BBB</h2>
</body>
// test.js
let d = Date.now()
while (Date.now() < d + 1000 * 3) { }
- 可以看到一开始就会渲染出
AAA,3s后才渲染出BBB - 说明外联脚本也会阻塞DOM解析与渲染,但是
因为无法确定脚本中的内容,所以会优先渲染一次已经构建DOM,确保加载的脚本能取得最新的DOM
如何解决JS阻塞渲染
1. <script>引入的位置
- 如果页面渲染内容为
<script>标签请求的内容,则该<script>标签一般需要放在<head>里面 - 如果页面渲染内容跟
<script>标签内容无关的话,比如说 DOM 事件、加载其他(还未见的)内容,则该<script>标签一般放在<body>标签里的最后位置
2. defer 和 async 属性
- 如果需要某段 JavaScript 代码需要提前加载,即可能会放在
<head>里面或某些 DOM 节点前面,则给<script>标签添加 defer 或 async 属性:- 如果加载完需要 立刻执行 则使用
async属性; - 如果加载完不需要立刻执行,想要在页面结构加载完(
window.onload)再立刻执行的话,使用defer属性; 值得注意的是:
- 如果加载完需要 立刻执行 则使用
- 没有使用这两种属性之一的话,则 JavaScript 的加载和运行都会阻塞渲染;
- 使用这两种属性之一的话,则 JavaScript 的
加载不会阻塞渲染,但运行仍会阻塞渲染
<body>
<h1>AAA</h1>
<script defer/async src="./test.js"></script>
<h2>BBB</h2>
</body>
defer 和 async 的区别
我们由一张图直观看出
- 绿色线 代表
DOM 解析 - 灰色线 代表
DOM 解析被阻塞期间 - 紫色线 代表
JS 脚本的网络读取 - 红色线 代表
JS 脚本的执行时间很明显看出 defer 和 async 的区别在于: - async 加载完
即刻执行,此时阻塞 DOM 解析 - defer 加载完
等待 DOM 解析完成后再执行 从此可以总结出 defer 和 async 的应用场景区别: - 如果你的脚本代码
依赖于页面中的DOM元素(文档是否解析完毕),或者被其他脚本文件依赖,则使用 defer 属性 比如:评论框、代码语法高亮 - 如果你的脚本并
不关心页面中的DOM元素(文档是否解析完毕),并且也不会产生其他脚本需要的数据,则使用 async 属性 比如:百度统计
附加问题
DOMContentLoaded 和 onLoad 的区别
- DOMContentLoaded: 是当初始的 HTML 文档被完全加载和解析完成之后,该事件被触发,而无需等待 CSS 样式表、图像和子框架的完全加载
- onLoad: HTML 文档、 CSS 样式表、JavaScript 代码、图像和子框架等都完全加载之后才触发
如何解决CSS阻塞渲染?
- 针对阻塞 DOM 解析——CSS引入位置
- 针对阻塞 CSSOM 树的构建——媒体查询
如何解决JS阻塞渲染?
- JS 引入位置
- defer 和 async 属性
defer和async的区别
- 原理上:加载和执行是否分开
- 应用上:脚本是否需要 DOM 解析完成才执行