HTML 渲染那些事儿

14,023 阅读23分钟

导读

最近一段时间刚好在公司内部涉及一些老旧项目的优化,所以对于 Web 网页性能方面沉淀了一些自己的看法。

恰好也参与了一些新同学的面试,发现大多数同学对于浏览器的渲染原理也只是一知半解。

所以,借着这个机会刚好来和大家聊聊浏览器是如何将你的 HTML 一步一步渲染到页面上的以及 JS 和 Css 在一过程中究竟是否会阻塞(延迟)这一过程。

写在前边

诚然,性能优化并不只是包含于浏览器的渲染优化。

但是,在笔者看来只有我们真正了解浏览器是如何将 HTML 渲染到页面上这一过程,在真正落地网页优化性能时才能做到所谓的心中有数,而不是人云亦云的添加一些优化参数或者属性。

接下来这篇文章我并不会和你仅仅讨论粗糙的理论知识,我会在所有理论知识上加以自己的实践进行论证帮助大家辅助理解这一过程。

文章主要围绕下四个方面进行展开:

  • 浏览器是如何将我们的 HTML 渲染到屏幕上的。

  • JavaScript 到底会不会阻塞你的页面渲染?

  • 那么,Css 呢?Css 对于页面渲染又存在什么影响。

接下来,让我们开始一探究竟吧。

浏览器是如何将我们的 HTML 渲染到屏幕上的

作为文章开头的第一部分 “浏览器是如何将我们的 HTML 渲染到屏幕上的” 我相信大多数同学都了解过这方面的知识。

让我们先从这一部分出来,来聊聊浏览器将 HTML 渲染到我们页面上会经历哪些步骤。

关键渲染路径

在浏览器接收到一个 HTML 文档时,粗糙的来说会经历一个所谓叫做关键渲染路径的步骤,最终将我们的文档渲染到页面上。

关键渲染路径包含了五个步骤, 构造文档对象模型(DOM),构造CSS 对象模型(CSSOM),生成渲染树、布局以及绘制。

开头的第一个部分,我们一个简单的 HTML 举例先来聊聊这所谓的关键渲染路径究竟发生了什么事情。

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
  </body>
</html>

构造文档对象模型(Dom)

以上面的 HTML 文档举例,当浏览器访问网页接受到 HTML 文档时,会对于接受到的 HTML 进行所谓的 Parsing 过程(解析 HTML 文档)。

这一过程主要在做的事情可以分为以下四个小阶段:

  1. 转化

    首先浏览器从磁盘(缓存)或网络读取 HTML 的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换为单个字符。

  2. 分词(词法分析)

    接下来浏览器会将上一步骤得到的字符串转换为一个一个 Token ——如W3C HTML5 标准所指定的,例如<html><body>——以及尖括号内的其他字符串。每个 Token 都有特殊的含义和自己的一套规则。

  3. 分析(语法分析)

    浏览器会将上一步的到的 Tokens 转换为一个 “对象”,这些对象定义了它们的属性和规则。

  4. 构造 DomTree

    构造文档对象模型的最后一步就是构建 DomTree。因为 HTML 中定义了不同标签之间的关系(一些标签包含在其他标签中等等),所以最终浏览器的到的对象是一个树型结构,该结构中通过嵌套等关系描述了文档中不同标签的关系。

image.png

整个过程会将 HTML 大概处理成为上述类型的树状结构, 最终输出文档对象模型 (DOM),之后浏览器会使用该对象模型进行其他处理。

构造CSS 对象模型(CSSOM)

当浏览器构建上述的 DOM 时,在 HTML 内部它还引用了一个外部 CSS 样式表 style.css。上述的 style.css 会返回以下内容:

body {
  font-size: 16px;
}
p {
  font-weight: bold;
}
span {
  color: red;
}
p span {
  display: none;
}
img {
  float: right;
}

Css 的处理和 HTML 差不多,浏览器也需要将接收到的 CSS 规则转换为浏览器可以理解和使用的内容。因此,对于 Css 浏览器仍然会重复上述的 4 个过程:

image.png

Css 文件经过转化为字符,然后进行分词、转化为节点最终拼接为一个树状的 Cssom。

image.png

当然,有部分同学会有疑问为什么 Css 也需要树状结构。其实之所以是将 Css 也处理也树状结构,原因为非常简单。

因为 Css 的规则是支持“向下级联”的嵌套方案的,也就是我们在日常开发中 Css 的继承特性。

浏览器在计算任何节点的样式时,它会从适用于该节点的最通用(顶层)的规则开始进行计算。比如,如果需要计算的节点是 body 元素的子元素,那么它会应用 body 的样式,之后会一层一层进行递归该过程从而得到该节点最终的样式。

这里额外有一些概念需要大家注意:通常情况下,大多数同学都认为 Css Parse 和 Dom Parse 是并行的关系。

其实,结论并不是这样的。

如果你的 css 是写在 style 标签中,那么在 chrome 中 style 标签会被 Html Parse 来解析。

大多数情况下,我们的 css 文件都会使用外部链接的方式进行引入,当使用 link 标签引入 Css 文件时。

由于 Cssom 的生成时机并不会影响 DomTree 的改变(JS 文件有可能会,因为我们可以通过 JS 修改操纵 Dom),自然当 HTML Parse 遇到 link 标签的 stylesheet 时并不会等待 stylesheet 下载并解析完毕后才会解析后续 Dom。

而是在网络进程加载 style 脚本的同时可以继续去解析后续 Dom,这是所谓的 Dom 和 Cssom 的并行关系(或者严格来说的非阻塞关系)。

而当网络进程加载完样式脚本后,主线程中仍然需要存在一个 parse styleSheet 的操作,这一步就是解析 link 脚本中的样式内容从而生成(添加)Cssom 上的节点。

需要注意的是,所谓 parse styleSheet 的操作是在主线程中进行操作的。这也就意味着它会和 parse Html 抢占主线程资源(同一时间只能进行一个操作)。

所以,通常我们所说 Dom 和 Cssom 生成的非阻塞更多的是指加载样式脚本并不会阻塞后续 Dom 解析(这里的非阻塞更多相对于 JS 文件来说,因为同步 JS 文件的加载是会阻塞后续 Dom 构建的),而主线程解析生成 cssom 的过程一定是会和 parse Html 抢占主线程资源的(主线程并不会同时 parse Html 以及同时 parse stylesheet)。

生成渲染树

上述的两个过程中,我们基于 HTML 和 CSS 分别的到了 DomTree 以及 CssomTree。当然,此时这两棵树都是互相独立的两个树状对象。

DomTree 描述了页面中所有的 Dom 结构内容,CssTree 描述了需要应用在页面节点上的样式规则。

接下来,要将一个完整的页面渲染给用户,自然浏览器需要做的是将两者进行合并。

上述的过程结束后,浏览器会将两个 Tree 进行合并,最终组成一个具有所有可见节点样式和内容的 Render Tree 。

image.png

上述渲染树的构建过程大概分为以下三个步骤:

  1. 从 DomTree 开始遍历,遍历每一个可见节点。

    • 一些脚本标签、元标签等节点是不可见的,由于它们未反映在页面的呈现中所以会被被省略。

    • 同时对于一些通过 CSS 隐藏的节点,也会从渲染树中省略。比如,上述 HTML 中的 span 节点在上面的例子中会在渲染树中丢失,因为它明确的设置了 “display: none” 属性。

  2. 对于 DomTree 中的每个可见节点,在 Cssom 中找到合适的匹配 CSSOM 规则并应用它们。

  3. 最终在 Render Tree 上挂载这些带有内容以及样式的可见节点。

需要注意的是,不可见元素并不代表不能被看到的元素。比如 visibility: hidden 不同于 display: none. 前者使元素不可见,但元素在布局中仍然占据空间(渲染为空框),而后者display: none表示将元素从渲染树中完全移除,使元素不可见从而不是布局的一部分。

最终,经过上述步骤浏览器会组装 DomTree 和 CssomTree 成为 RenderTree,RenderTree 中包含屏幕上所有可见内容的内容和样式信息

布局

在 RenderTree 构建完毕后,接下来就会进入布局 Layout 阶段了。

得到 RenderTree 后,浏览器已经明确的清楚哪些节点应该被渲染到页面上同事也获得了可见节点的样式。

但是,此时浏览器并未计算出每个节点在对应设备(屏幕)上确切的位置和大小。这一步也就所谓布局(Layout)阶段应该处理的事情。

布局过程的输出是一个“盒子模型”,它精确地捕获视口内每个元素的确切位置和大小:所有相对测量值都转换为屏幕上的绝对像素。

绘制

一旦渲染树创建并且布局完成,像素就可以被绘制在屏幕上,既然浏览器已经明确的知道哪些节点是可见的,以及它们的样式和几何形状,我们可以将此信息传递到最后阶段,它将 RenderTree 中的每个节点转换为屏幕上的实际像素,此步骤通常称为“Paint”。

经过绘制阶段,最终浏览器中会呈现出 HTML 渲染完毕的结果。

上述我们简单的描述了一次关键渲染路径所发生的事情:

image.png

当然,这之中有非常多其他值得深究的点,比如 Composite 阶段的不同 Layer 层之类等等。

网络上相关关键渲染路径的文章已经非常多了,这里我就不过于累赘了。

这部分内容主要是想让大家对于一次浏览器渲染发生的事情有一个大概的认知,方便我们继续后续的内容。

JavaScript 到底会不会阻塞你的页面渲染?

上边的章节我们简单聊了聊一次浏览器渲染究竟会发生哪些步骤。

image.png

实在是懒的画了,这是我从网络上搬运过来的一张图,浏览器本质上是一个多进程模型,上述的渲染过程是会发生在浏览器渲染进程中渲染线程中进行执行(这句话稍微有点绕)。

熟悉浏览器的小伙伴,大概率也听过渲染进程中的另一个线程:Js 引擎线程。

当然,Js 引擎线程负责 JavaScript 代码的解析和执行,而渲染线程则负责具体页面的解析和渲染(比如上述的 HTML Parse 过程)。

再简单来说,Js 引擎线程和渲染线程这两者是互斥的

当 HTML 下载时,Parse HTML (上述关键渲染路径中生成 DomTree)的过程如果碰到 JS 脚本是会停止后续 Dom 的解析的。

那么,换句话来聊聊。HTML 中的 JavaScript 会阻塞页面的渲染吗?

网络上绝大多数文章都是片面的告诉你结论: JS 会阻塞页面渲染,不过结果真的是这样吗?

其实上边这个问题比较笼统,是没法直接进行回答的(起码我在面试过程中从未有候选人会对于问题再次发出提问)。

首先,在 HTML 加载 JavaScript 存在两种方式,一种为内敛脚本也就是直接将 JS 写在 HTML 中,另一个中称为外部资源,也就通过 script 脚本加载的外部资源。

另外 JavaScript 产生的阻塞,指的是加载(DownLoad)Js 文件时,还是执行 Js 文件时这又是另一个话题。

接下来,我们分为不同情况来看看不同情况下的 JS 对于是否阻塞页面渲染的不同表现。

文章中的 HTML 执行在 Chrome 108.0.5359.71 正式版本。

内联 Js

首先,我们来看这样一段代码:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div>Hello</div>
  <p>My Name is 19Qingfeng</p>
  <p>Welcome follow my juejin!</p>
  <script>
    const now = Date.now()
    while (Date.now() - now < 500) {
      // block
    }
  </script>
</body>

</html>

上述的代码非常简单,我们在 HTML 脚本中做了一个内联脚本进行了 500 ms 的 block 。

正常来说,首先浏览器会对于 HTML 文件进行 parsing ,也就是所谓 Parse Dom 生成 DomTree 的过程。

不过在解析到 script 脚本之后,应该会将主线程交由 JS 引擎线程进行执行 JS,从而 DomTree 生成的过程。

那么不难想象,在构建 DomTree 的过程中渲染线程被 JS 抢去了,自然也没法继续进行渲染了不是。

所以对于内联脚本的情况,JS 不存在加载(本身就是内联上哪加载去),而 JS 的解析和执行是一定会阻塞页面的渲染的。

下图是我用 Performance 这个 Demo 页面的简单分析截图:

image.png

明显可以看到,DCL(DomContentLoaded)之后才会出现 FP(First Paint 首次绘制时间点),自然这也就明确表示Dom 解析完成后过了一段时间(这段时间做的事情我们刚刚在第一阶段提到过)页面才会进行渲染。

当然,上古时期很多同学或多或少会听过一句经典的面试题“为什么 css 放上边,html 放在底部”。

css 的问题,我们后续再说,而对于 js 放在底部对于内联脚本和页面渲染来说并没有什么太大的区别。

你把内联脚本放在哪里都是会阻塞页面的渲染,不过是放在底部在脚本中可以拿到内存中已经构造好的 Dom 节点进行 Dom 操作而已。

外链 JS

讨论完内联 JS 的事情,我们再来看看外链 JS 的问题。如果 HTML 中的 JavaScript 是外部脚本,那么它的加载和执行是否会阻塞页面渲染呢?

预解析

首先,我们需要明确的是 HTML 文档的解析过程中同时会存在一个所谓的预解析过程,这一过程会分析 HTML 中的外部资源链接从而并不需要在进行 HTML Parse 的过程中发现外部资源才会进行下载,而是在预解析的过程中提前进行外部资源的下载。

比如刚才的 HTML 中我将 js 脚本拆分为外部脚本index.js

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div>Hello</div>
  <p>My Name is 19Qingfeng</p>
  <p>Welcome follow my juejin!</p>
  <script src="./index.js"></script>
</body>

</html>

image.png

注意图中的四个位置,在 Parse HTML 的 Task 前一个 Task 在 Event Log 中我们已经可以看到发送了请求 index.js 的请求,而非是在 Parse HTML 阶段才发出的请求哦。

那么,外部资源是否会阻塞页面渲染呢?其实答案并不是那么绝对。

外链资源会阻塞页面渲染吗?

首先,外部 JS 资源的确可能会阻塞页面的渲染,不过这也是分情况而论。

这里我们先不考虑 async 和 defer 的情况,单纯只考虑外部 JS 脚本。

无论是 JS 资源的加载和执行,我们有一个明确的前提:当 Parse Html 的过程中如果碰到外部 JS 脚本,那么外链脚本的确是会停止解析后续 Dom 的,但是停止解析后续 Dom 并不意味着一定会阻塞页面的渲染。

情况1: JS 脚本在顶部

首先,我将上述的 HTML 中的外部 script 脚本移动 head 标签中:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="./index.js"></script>


</head>

<body>
  <div>Hello</div>
  <p>My Name is 19Qingfeng</p>
  <p>Welcome follow my juejin!</p>

</body>

</html>

我们这次来重新看看它的表现如何:

image.png

结果一目了然,页面中的 FP 和 DCL 的时间相差无几。简单来说,将外部标签写在 head 标签之后的确是会阻塞后续 DOM 的渲染的。

感兴趣的同学私底下可以自己尝试下利用 performance 测试下,同时注意关闭一些插件以及 sw 文件避免混淆视听。

情况2: JS 脚本在底部

同样,我们再来看看当把 JS 放在底部时应该表现如何:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>


</head>

<body>
  <div>Hello</div>
  <p>My Name is 19Qingfeng</p>
  <p>Welcome follow my juejin!</p>
  <script src="./index.js"></script>
</body>

</html>

其实结果一目了然,在执行 JS 之前浏览器已经进行了首次绘制。

换句话说,外链的 index.js 的确阻止了 Dom Parse 这个一过程,因为我们可以清晰的看到 DCL 是在 js 脚本执行完毕后解析完毕生成 DomTree 才会完成。

而 index.js 之前的 Dom 节点的渲染并没有被阻塞,LCP 和 FP 出现在了明显在 JS 执行之前开始。

image.png

相信经过两次对比,大家也都已经清楚所谓 JS 资源的阻塞并不是意味着 JS 未执行完毕之前页面一直会是白屏状态(当然,如果你的应用无任何骨架屏的 SPA 应用那么其实JS未执行完毕就是白屏)。

*外部脚本链接的加载和执行只会影响后续 Dom 的解析和渲染,对于脚本之前的的 Dom 并不会阻塞它的解析以及渲染,这也就是为什么我们常说将 js 放在底部。

情况3: defer & async

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

上述是 MDN 关于 defer 属性的描述,文档解析完成后,触发 DCL 之前执行。

那么其实答案已经非常明显了,如果外部脚步标记为 defer 后,此时文档解析完毕会立即触发一次渲染之后才会去依次执行标记为 defer 的脚本。

简单来说,也就是标记为 defer 的脚本并不会阻塞页面的首次渲染。我们尝试将上述 HTML 中的脚本标记为 defer 。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script defer src="./index.js"></script>
</head>

<body>
  <div>Hello</div>
  <p>My Name is 19Qingfeng</p>
  <p>Welcome follow my juejin!</p>
</body>

</html>

image.png

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

当然,async 标记的普通脚本和 defer 的原理是相通的。

虽然 async 的执行时机并不确定,但由于 async 也同为异步脚本的特性,所以标记为 async 属性的脚本也不会阻塞后续 Dom 渲染(当然由于它的执行时机不确定性,脚本在 Receive response & Finish loading 后立即执行,所以 async 究竟会阻塞哪些节点渲染在不同网络条件下是不尽相同的)。

那么,Css 呢?

在探讨完 JS 脚本对于页面渲染的阻塞后,我们再来看看 Css 文件呢。

日常业务项目中,无论是基于各种构建工具还是自己手撸各种架子。大多数情况下,我们都会将生成的 script 脚本标记上 defer 属性。

自然,标记为 defer 的脚本刚刚我们也有结论并不会阻塞页面的首屏渲染~接下来我们来看看所谓的 Css 又是如何表现的呢。

Css 是否会阻塞页面渲染

无论是 Css 还是 Js 文件,都会存在两种模式一种是内联一种是外部脚本。

这里之所以并没有将 Css 文件拆分为内联方式、外链方式来讲的原因是,无论是哪种方式的 Css 都会阻塞后续 Dom 节点渲染。

上述在关键渲染路径中,我们提到过页面的渲染是由 DomTree 和 Cssom 结合而来的 RenderTree 。

对于 JavaScript 文件的确是会阻塞后续 Dom Parse 过程,但是并不会阻塞之前节点的渲染。

而对于 Css 文件,在进行 HTML Parse 时如果碰到了外部 Link 标签是会将外部 Link 交给网络进程来异步下载。

而对于 Css 文件的解析,由于页面中的 Css 并未解析完成的话,此时是会阻塞页面的渲染的。(当然,如果样式脚本没有下载完毕当然也不会被解析完毕)

我们以下面一段简单的代码来做对比:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="./index.css" />
</head>

<body>
  <div>Hello</div>
  <p>My Name is 19Qingfeng</p>
  <p>Welcome follow my juejin!</p>
</body>

</html>
// index.css
body {
  color: red;
}

上述的代码非常简单,在 html 脚本中我们加载一个外部 stylesheet 脚本,index.css 中仅仅是将样式文字样式变为红色。

此时,打开 Performance 我们来一探究竟:

image.png

测量时我刻意将网络调整为 slow 3G。

上述为页面首次加载时的 performance 面板,首先我们可以对比网络进程和主进程中,明确的可以看到当 css 文件加载完毕之后才会触发页面的 FP 时间点。

其次,图中有两个重要的时间点,分别为页面 DCL 的时间的 FP 的时间。

  • 首先我们来看看图中的第一个时间点:DCL 的时间点。我们清楚的可以看到差不多在 2037ms 阶段已经解析完所有的 Dom 了(因为加载外部 link 标签并不会阻塞后续 Dom 的解析,当然这也不是绝对的),证明 Dom Tree 已经构建完毕。

image.png

但是此时页面并没有渲染任何东西,仍然是需要等待 Css 文件加载完成,相当于触发 onload 之后才进行了渲染。(因为 HTML 中仅有一个 css 外部链接,自然 css 文件加载完毕就会触发 onload 事件)。

image.png

我们可以清晰的看到在 4060.2ms 开始 Receive Data 收到 css 文件的数据同时开始一系列 Parse 以及后续的 Layout (当然下个 Task 中还有 paint 等动作)。

最终差不多在 4060ms 左右开始触发 FP,浏览器第一次向屏幕传输像素,表示真正页面才开始渲染。

那么我们来换一种写法呢?

此时,我们稍作改动。将原本放在顶部的 css 文件放置在 body 底部:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <!-- <script async src="./index.js"></script> -->
</head>

<body>
  <div>Hello</div>
  <p>My Name is 19Qingfeng</p>
  <p>Welcome follow my juejin!</p>
  <link rel="stylesheet" href="./index.css" />

</body>

</html>

此时我们再来看看会发生什么。

image.png

我们会惊奇的发现,页面会首次渲染出所谓的无样式内容(并不存在文字颜色),之后过一段时间等待 Css 加载完成在此未之前的 Dom 添加上样式。(其实这就是所谓的重绘过程)

这个过程其实也不难理解,当主线程 Parse Html 的过程中遇到 link 标签会立即进行一次绘制,此时已经解析好的 DomTree 会被认为 Cssom 依赖(本来一直到 link 标签前也没有任何样式内容需要加载)。

所以,首先会绘制一次无样式的 Dom 在屏幕上,之后等待 link 标签加载完毕并且解析完成 Cssom 和解析到的 DomTree 会生成 RenderTree 再次进行页面渲染(此时渲染的即是存在样式的内容了)。

当然,这样来看的确将外链 css 脚本放在底部页面的 FP 会特别快,不过这就牵扯到另一个问题了。

对于页面渲染来说,短暂的无样式页面展示给用户是否真正有必要,以及对于浏览器来说页面的重绘和回流成本是巨大的。

Css 是否会阻塞 Dom 解析

这里也不对,有需要斟酌的地方。

至于 Css 是否会影响 Dom 解析,当然 Cssom 的生成是在 DomTree 构建之后发生。那么外部 Css 脚本的加载是否会影响后续 Dom 元素的解析呢?

换句话说,Html 中的 link 标签是否会阻塞后续 Dom 解析?我相信大多数同学的回答都是不会,这也是最近我在几场面试过程中得到的回答。

其实,答案并没有那么绝对。或者准确的是说,会也不会?

这里我直接先放结论:

  1. 普通情况下css 脚本的加载并不会阻塞后续 Dom Tree 的生成。(Css 文件加载不阻塞解析特性)

  2. 同时 css 脚本的加载是会阻塞 RenderTree 的合成,从而阻塞页面的渲染(Css 文件加载渲染阻塞特性)。

  3. 特殊情况下,比如 link 的样式脚本后存在 JS 文件,那么此时 Css 代码的加载是会阻塞后续 JS 脚本的执行从而 JS 脚本会阻塞后续 Dom 的解析,从而变相相当于阻塞了 Js 代码之后的 Dom 解析

注意第三点中写到的脚本,指的是非 defer 以及 async 标记的异步脚本。如果是异步脚本,脚本本身并不会阻塞后续 Dom 的解析,自然 css 也并不会阻塞。

至于这一结论在这篇文章中我并不会去论证它是如何得到的,如果感兴趣的同学可以参考我的另一篇文章 一次useEffect引发浏览器执行机制的思考

稍微总结一下 Css

所以,如果你在乎页面首次渲染时间。其实过多的关心 JS 而忽略 Css 文件恰恰会适得其反。绝大多数时候影响页面首屏渲染的时机恰恰是 css 文件在作祟。

当然我并没有说 js 文件不重要,只是不同场景下不能一概而论,而 css 文件日常工作中会被大多同学忽略,但的确 Css 对于网页渲染的重要性丝毫不亚于 Js 。

结尾

文章到这里就和大家说声再见了,如果大家有任何疑问也可以在评论区我们一起探讨。

当然关于浏览器的渲染机制其实远比文章中描述的复杂的多的多,这篇文章我更多的是希望带给大家带来抛砖引玉的作用。

希望通过这篇文章大家对于浏览器的基本渲染原理有一个大概的了解,以后对于性能优化并不只是人云亦云的添加一些属性,而是真正可以做到“知其然知其所以然”。