阻塞渲染分为两类:
CSS阻塞渲染
从浏览器系列 — 渲染原理及过程中可得知第3步在构建渲染树时,需要完备的DOM树,CSSOM树,而CSSOM的解析可能会阻塞DOM解析
,或者CSSOM树的未完成会阻塞渲染树的构建
,这就是CSS阻塞渲染
JS阻塞渲染
JS 可以查询和修改 DOM 与 CSSOM
,所以当 HTML 解析器遇到一个 script 标记时,它会暂停构建 DOM,将控制权移交给 JS 引擎, HTML 解析器会等待 JS 引擎运行完毕
,这就是所谓的JS阻塞渲染
CSS阻塞渲染
1. 阻塞 DOM 解析(DOM树一直无法构建出来)
<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节点才会被解析到,然后渲染出来
2. 阻塞渲染树的构建(CSSOM树一直无法构建出来)
<head>
<link rel="stylesheet" href="https://www.youtube.com/file.css"><!--这里会卡一段时间-->
<link rel="stylesheet" href="./link.css">
</head>
复制代码
- 在外网样式资源下载完成前,页面将会处于白屏现象
- 如阻塞渲染的样式资源下载超时报错,则会跳过,会使用已经下载完成的CSS资源做解析构建CSSOM
- 所以在等待一段时间后(资源下载超时后)页面才会显示出来
如何解决CSS阻塞渲染
1. CSS引入的位置——针对阻塞 DOM 解析
如果把<style>
、<link>
标签放在<body>
里面,比如像上面<link>
就会阻塞后面的<h1>
DOM 节点的解析
一般我们把<style>
、<link>
放在<head>
里面,提前加载好CSS资源,那么<h1>
DOM 节点要解析时<link>
那部分可能已经“完成”(超时跳过)了
2. 媒体查询的方式——针对阻塞CSSOM树的构建
<!-- 适用于所有情况,始终阻塞渲染 -->
<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>
标签放在<body>
的最后的位置,先进行 DOM 解析再加载/执行 JS 脚本,比如像上面<script>
就会阻塞整个页面的渲染,使页面处于白屏状态
如果我们把<link>
放在<head>
里面,提前加载好CSS资源,那么<h1>
DOM 节点要解析时<link>
那部分可能已经“完成”(超时跳过)了
2. defer 和 async 属性
内联脚本阻塞渲染
在上面内联JS脚本阻塞渲染的例子中使用defer
或async
属性
<body>
<h1>AAA</h1>
<script defer/async>
let d = Date.now()
while (Date.now() < d + 1000 * 3) { }
</script>
<h2>BBB</h2>
</body>
复制代码
这里即使使用了defer
或async
属性也无济于事,说明这两种属性不能
解决内联脚本阻塞问题
外联脚本阻塞渲染
在上面外联JS脚本阻塞渲染的例子中如果使用defer
或async
属性
<body>
<h1>AAA</h1>
<script defer/async src="./test.js"></script>
<h2>BBB</h2>
</body>
复制代码
// test.js
let d = Date.now()
while (Date.now() < d + 1000 * 3) { }
console.log('render success');
复制代码
这里可以看到一开始就会渲染出AAA
、BBB
,3s后打印render success
,说明这两种属性可以解决外联脚本阻塞问题
既有内联脚本又有外联脚本
<body>
<h1>AAA</h1>
<script defer/async src="./test.js"></script>
<script>
let k = Date.now()
while (Date.now() < k + 1000 * 4) { }
</script>
<h1>BBB</h1>
</body>
复制代码
// test.js
let d = Date.now()
while (Date.now() < d + 1000 * 4) { }
console.log('render success');
复制代码
- 4s 后可以看到渲染出
AAA
- 又经过 4s 后渲染出
BBB
,同时控制台打印render success
如果去掉defer
/async
则AAA
和BBB
需要经过8s
才会渲染出来,并且这期间一直处于页面白屏
的状态
defer 和 async 的区别
我们由一张图直观看出
- 绿色线 代表
DOM 解析
- 灰色线 代表
DOM 解析被阻塞期间
- 紫色线 代表
JS 脚本的网络读取
- 红色线 代表
JS 脚本的执行时间
很明显看出 defer 和 async 的区别在于:
- async 加载完
即刻执行
,此时阻塞 DOM 解析 - defer 加载完
等待 DOM 解析
完成后再执行
从此可以总结出 defer 和 async 的应用场景区别:
- 如果你的脚本代码
依赖于
页面中的DOM元素(文档是否解析完毕
),或者被其他脚本文件依赖
,则使用 defer 属性
比如:评论框、代码语法高亮
- 如果你的脚本并
不关心
页面中的DOM元素(文档是否解析完毕
),并且也不会产生
其他脚本需要的数据,则使用 async 属性
比如:百度统计
附加问题
如何解决CSS阻塞渲染?
- 针对阻塞 DOM 解析——CSS引入位置
- 针对阻塞 CSSOM 树的构建——媒体查询
如何解决JS阻塞渲染?
- JS 引入位置
- defer 和 async 属性
defer和async的区别
- 原理上:加载和执行是否分开
- 应用上:脚本是否需要 DOM 解析完成才执行