JS CSS 是如何阻塞页面渲染的

TLDR,CSS 和 JS 都会阻塞页面的 关键渲染路径 ,不同情况下阻塞效果不同:

  • 内联 JS 的执行会阻塞 DOM 构建(Layout)
  • 外部 JS 的加载执行都会阻塞 DOM 构建
  • 内联 CSS 的 CSSOM 构建会阻塞渲染树的构建,从而阻塞布局(Layout)
  • 外部 CSS 的加载 CSSOM 构建都会阻塞渲染树的构建,从而阻塞布局
  • 在页面存在 CSS 和 JS 的情况下,CSSOM 的构建会阻塞他后面 JS 的执行
  • script 标签的 async 和 derfer 属性能使 JS 的加载不阻塞 DOM 构建

前情提要

在学习 JS、CSS 如何阻塞页面渲染之前,我们需先了解 浏览器是怎样渲染页面的

一个测试 Demo

为了验证关键渲染路径是如何被阻塞,可以运行一个 Web Server,并通过浏览器调试工具(Chrome DevTools – Performance)观察页面渲染过程。

一个简单的 Demo,目录结构如下:

demo
├─ index.js
└─ static
       └─ index.html

index.js 是一个基于 Express 的 Web Server。这里指定了 ./static 为静态资源目录,通过参数 t 模拟在网络请求上花费的时间。

const express = require('express')
const app = express()
const port = 3000


// static
app.use(blockRequest, express.static('./static'))


// listen port
app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})


/**
 * block request
 */
function blockRequest  (req, res, next) {
  const { t } = req.query
  setTimeout(next, t)
}


我们通过修改修改 static/index.html 来观察不同情况下浏览器关键渲染路径的阻塞情况。

JS 是如何阻塞页面渲染的

由于 JS 可能会修改 DOM,所以运行 JS 和构建 DOM 不能同时进行,所以 JS 会对页面渲染造成较大的影响。

验证 Js 对页面渲染的影响主要分为三种情况:

  1. 内联方式的 JS
  2. 外部链接引入的 JS
  3. 外部链接并使用 async、defer 属性的 JS

内联 JS 对页面渲染的影响

我们将 static/index.html 修改为如下内容,观察内联方式 JS 对页面渲染的影响:

<html>
<body>
  <h1>hello</h1>

  <script>
    const stopTime = Date.now() + 20
    while (Date.now() < stopTime);
  </script>

  <br>
  <h1>world</h1>
</body>
</html>

进入无痕模式(排除 Chrome 插件影响),进入 Performance 工具开启性能分析并刷新页面,得到如下结果:

内联 JS

浏览器接收到 HTML 内容便立即开始 Parse HTML(构建 DOM) ,点击 Parse HTML 可以看到详细的执行耗时。如下:

Parse HTML 阶段耗时

Parse HTML(构建 DOM)只需要 0.3ms,而 JS 却阻塞了 19.9ms,整个 Parse HTML 过程花费了约 20.1 ms。

假设 JS 代码执行需要更久的时间,则 Parse HTML 也会被拉长更久,无法进入下一步(Layout)。即内联 JS 的执行会阻塞关键渲染路径

外链 JS 对页面渲染的影响

static/index.html 修改为如下内容,观察通过外部链接引入 JS 对页面渲染的影响:

<html>
<body>
  <h1>hello</h1>

  <script src="/block.js?t=100"></script>

  <br>
  <h1>world</h1>
</body>
</html>

得到如下结果:

外链 JS

与内联 JS 不同的是 Parse HTML 被分为两次了。

在首次 Parse HTML 中,浏览器解析了 index.html 中 script 标签之前的内容(0 – 3 行)并立即发起请求获取 block.js,停止了解析。

在等待 block.js 加载的过程中,浏览器没有对 script 标签后面的内容进行解析,而是对已经完成解析的这部分内容进行了布局和绘制(Paint)。首次绘制(FP)的时间早于 block.js 加载完成的时间,但此时页面上显示 hello,内容并不完整。

block.js 加载完成后,浏览器没有对后面的 HTML 解析,而是先执行了 JS 代码。直到代码执行完成后才开始对剩余内容解析、回流、重绘。

在等待外部 JS 加载完成的过程中,浏览器无法对 script 标签后面的内容进行解析,即 JS 的加载会阻塞 DOM 构建。外部 JS 加载完成后需要先执行 JS,即 JS 的执行也会阻塞 DOM 构建。

可以得出结论,外链形式引入 JS 的加载、执行都会阻塞关键渲染路径。

async、derfer 属性对页面渲染的影响

MDN 对这两个属性的解释:

async: 对于普通脚本,如果存在 async 属性,那么普通脚本会被并行请求,并尽快解析和执行。

defer: 这个布尔属性被设定用来通知浏览器该脚本将在文档完成解析后,触发 DOMContentLoaded 事件前执行。有 defer 属性的脚本会阻止 DOMContentLoaded 事件,直到脚本被加载并且解析完成。

MDN 文档:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script

在上面 static/index.html 的基础上,分别加上 async、defer 属性:

<html>
<body>
  <h1>hello</h1>

  <script async src="/block.js?t=100"></script>

  <br>
  <h1>world</h1>
</body>
</html>

使用 async 属性时如下:

使用 async 属性

使用 defer 属性时如下:

使用 defer 属性

可以发现,加上 async、defer 这两个属性后,block.js 的加载都没有影响到 Parse HTML。

浏览器接收到 HTML 后立即开始了页面的解析,当遇到了 script 标签便立即发起了请求去加载 block.js。与之前不同的是,浏览器继续对 script 后面的内容进行了解析、布局、绘制。

可以得出结论,async、defer 属性都可以使外链引入的 JS 不阻塞 Parse HTML(DOM 构建)

async、defer 的差异还有哪些可以查看:script 标签的 async 与 defer 属性有什么区别

CSS 资源如何阻塞页面渲染

不同于 JS,CSS 不会修改 DOM 所以不会阻塞构建 DOM,所以构建 DOM 与构建 CSSOM 井水不犯河水。内联的 CSS 已经在 HTML 中一同返回,所有不会对 DOM 构建产生影响。

模拟外链 CSS 阻塞页面渲染,将 static/index.html 修改为如下内容:

<html>
<head>
  <link rel="stylesheet" href="block.css?t=100  ">
</head>
<body>
  <h1>hello world</h1>
  <h1>balabala ...</h1>
</body>
</html>

得到如下结果:

CSS 阻塞页面渲染

浏览器是怎样渲染页面的 中我们知道浏览器需要将 DOM 和 CSSOM 合并成一颗渲染树(Render tree)后才能进行布局和绘制。

如果 CSS 通过外链引入,即使 CSS 不阻塞 DOM 构建,但在 CSS 加载完成之前都无法进行 CSSOM 构建也就无法进行渲染树的构建,从而导致阻塞关键渲染流程。

可以看到在 Parse HTML 完成后一直在等待 block.css 加载,直到 block.css 加载完成才进行 CSSOM 树的构建、渲染、绘制。证明 CSS 会导致关键渲染路径阻塞

当 JS 遇见 CSS

上述情况只讨论了 HTML + JS、HTML + CSS 的情况,如果 HTML、JS、CSS 都同时存在,对页面渲染又会有什么影响?

JS 之所以会阻塞 DOM 构建是因为 JS 可能会修改 DOM,所以只能按照顺序执行。

同理,JS 也有可能会修改样式,所以在 CSSOM 构建完成之前,JS 无法执行,也就是 CSSOM 会阻塞 JS 执行(前提是 JS 在 HTML 的位置位于 CSS 后面)。

从头到位捋一遍他们之间的关系:CSS 会阻塞渲染树的构建和他后面的 JS 执行,JS 的执行和 DOM 构建又是相互阻塞的。

修改 static/index.html 如下:

<html>
<body>
  <h1>hello</h1>
  <script src="/block.js?t=100"></script>
  <h1>world</h1>
  <link rel="stylesheet" href="/block.css?t=300">
  <script src="/block.js?t=200"></script>
</body>
</html>

观察 JS 的执行顺序:

HTML + JS +CSS

可以发现,即使 block.js?t=200block.css?t=300 先加载完成,但他并没有立即执行,而是等待 block.css?t=300 加载完成后才执行。

将 CSS 放在头部而将 JavaScript 放在尾部

将 CSS 放在页面头部的 head 标签中,将 JavaScript 放在页面尾部 </body> 之前

来源网络

在网络上查询性能优化的相关信息的时候总能发现有这样的建议,为什么 CSS 需要放在头部 JavaScript 需要放在页面底部?

CSS 为什么需要在页面头部

CSS 不会阻塞 DOM 构建,但却会阻塞渲染树构建,从而阻塞布局影响关键渲染流程。

如果将 CSS 放在页面中间或者底部,CSS 不会阻塞 DOM 构建,已经解析完成的内容会被渲染出来。这时一旦 CSS 加载完成页面则会重新渲染,可能造成页面变化。一方面不必要的重新渲染造成额外的性能负担,另一方面页面的变化体验也非常不好。

将 CSS 资源放在头部则能尽早开始加载,减少网络请求上阻塞的时间。

所以将重要的 CSS 资源放在页面头部避免页面阻塞提高渲染效率。另外对于体积较大的 CSS 资源建议以外部链接的方式加载,这样则能充分利用缓存减少请求时间。

JS 为什么需要在页面底部

script 标签在不设置 async、defer 属性的情况下,会阻塞 DOM 构建。

如果将这样的 script 标签放在页面头部,在没有加载完成的情况下不会解析 script 后面的 HTML 内容,直到 JS 加载执行完成页面都会一直显示空白,体验非常不好。

将 JS 放在页面底部则不会有这样的问题,即使浏览器解析 HTML 在底部被阻塞了 script 标签之前的内容依然可以显示,用户依然能在第一时间看到完整的页面。

不过在有了 async、derfer 属性之后,script 标签的位置已经不是那么重要了。现代的浏览器对页面解析和渲染也做了大量的工作,预加载扫描器等优化技术也极大的提升了渲染效率。

相关文章

浏览器是怎样渲染页面的 – 龚为的 Blog

关键渲染路径 – berwin/Blog

发表评论

邮箱地址不会被公开。 必填项已用*标注