浏览器系列 -- 阻塞渲染

1,176 阅读4分钟

阻塞渲染分为两类:

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>
  1. 可以看到页面依旧处于白屏状态
  2. 点击浏览器的停止加载按钮,red1内容会被渲染出来,此时查看Dom树,发现是没有<h1>red2</h1>这个节点的
  3. 当资源下载失败时,<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 的区别

我们由一张图直观看出

image.png

  • 绿色线 代表 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 解析完成才执行