阅读 67

浏览器系列 -- 阻塞渲染

阻塞渲染分为两类:

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

2. 阻塞渲染树的构建(CSSOM树一直无法构建出来)

<head>
    <link rel="stylesheet" href="https://www.youtube.com/file.css"><!--这里会卡一段时间-->
    <link rel="stylesheet" href="./link.css">
</head>
复制代码
  1. 在外网样式资源下载完成前,页面将会处于白屏现象
  2. 如阻塞渲染的样式资源下载超时报错,则会跳过,会使用已经下载完成的CSS资源做解析构建CSSOM
  3. 所以在等待一段时间后(资源下载超时后)页面才会显示出来

如何解决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脚本阻塞渲染的例子中使用deferasync属性

<body>
    <h1>AAA</h1>
    <script defer/async>
        let d = Date.now()
        while (Date.now() < d + 1000 * 3) { }
    </script>
    <h2>BBB</h2>
</body>
复制代码

这里即使使用了deferasync属性也无济于事,说明这两种属性不能解决内联脚本阻塞问题

外联脚本阻塞渲染

在上面外联JS脚本阻塞渲染的例子中如果使用deferasync属性

<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');
复制代码

这里可以看到一开始就会渲染出AAABBB,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/asyncAAABBB需要经过8s才会渲染出来,并且这期间一直处于页面白屏的状态

defer 和 async 的区别

我们由一张图直观看出

image.png

  • 绿色线 代表 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 解析完成才执行
文章分类
前端
文章标签