# css优先级

浏览器通过优先级来判断哪一些属性值与一个元素最为相关,从而在该元素上应用这些属性值。优先级是基于不同种类选择器组成的匹配规则。

<div id="content" class="content">
我是什么颜色
</div>
<style>
#content {
    color: #f00;
}
.content {
    color: #0f0;
}
</style>

答案很简单:红色。这就涉及到了优先级问题,同一块内容,我们同时用了 ID选择器 和 类选择器,因为 ID选择器 优先级大于 类选择器 , 所以最终显示为红色。

CSS选择器的优先级关系是:内联 > ID选择器 > 类选择器 > 标签选择器。但是,浏览器具体的优先级算法是怎样的?

优先级是由 A 、B、C、D 的值来决定的,其中它们的值计算规则如下:

  • 如果存在内联样式,那么 A = 1, 否则 A = 0;
  • B 的值等于 ID选择器 出现的次数;
  • C 的值等于 类选择器 和 属性选择器 和 伪类 出现的总次数;
  • D 的值等于 标签选择器 和 伪元素 出现的总次数 。

先上个例子:

#nav-global > ul > li > a.nav-link
li                                  /* (0, 0, 0, 1) */
ul li                               /* (0, 0, 0, 2) */
ul ol+li                            /* (0, 0, 0, 3) */
ul ol+li                            /* (0, 0, 0, 3) */
h1 + *[REL=up]                      /* (0, 0, 1, 1) */
ul ol li.red                        /* (0, 0, 1, 3) */
li.red.level                        /* (0, 0, 2, 1) */
a1.a2.a3.a4.a5.a6.a7.a8.a9.a10.a11  /* (0, 0, 11,0) */
#x34y                               /* (0, 1, 0, 0) */
li:first-child h2 .title            /* (0, 0, 2, 2) */
#nav .selected > a:hover            /* (0, 1, 2, 1) */
html body #nav .selected > a:hover  /* (0, 1, 2, 3) */

比较规则是: 从左往右依次进行比较 ,较大者胜出,如果相等,则继续往右移动一位进行比较 。如果4位全部相等,则后面的会覆盖前面的

# 深入理解浏览器解析渲染 HTML

写 HTML,CSS 和 Js,但是浏览器是如何解析这些文件,最终将它们以像素显示在屏幕上的呢?这一过程叫做 Critical Rendering Path。(关键渲染路径

指的是浏览器从请求 HTML,CSS,JavaScript 文件开始,到将它们最终以像素输出到屏幕上这一过程。包括以下几个部分:

  1. 构建 DOM
    • 将 HTML 解析成许多 Tokens
    • 将 Tokens 解析成 Objects
    • 将 Objects 组合成为一个 DOM 树
  2. 构建 CSSOM
    • 解析 CSS 文件,并构建出一个 CSSOM 树(过程类似于 DOM 构建)
  3. 构建 Render
    • 结合 DOM 和 CSSOM 构建出一颗 Render 树
  4. Layout
    • 计算出元素相对于 viewport 的相对位置
  5. Paint
    • 将 Render 树转换成像素,显示在屏幕上

值得注意的是,上面的过程并不是依次进行的,而是存在一定交叉。

想要提高网页加载速度,提升用户体验,就需要在第一次加载时让重要的元素尽快显示在屏幕上。而不是等所有元素全部准备就绪再显示,下面一幅图说明了这两种方式的差异:

# 构建 DOM

DOM (Document Object Model),翻译过来叫做文档对象模型。构建 DOM 是必不可少的一环,浏览器从发出请求开始到得到 HTML 文件后,第一件事就是解析 HTML 成许多 Tokens,再将 Tokens 转换成 Objects,最后将 Objects 组合成一棵 DOM 树。

这个过程是一个循序渐进的过程,假设 HTML 文件很大,一个 RTT (Round-Trip Time) 只能得到一部分,浏览器得到这部分之后就会开始构建 DOM,并不会等到整个文档就位才开始渲染。这样做可以加快构建过程,而且由于自顶向下构建,因此后面构建的不会对前面的造成影响。

后面将会提到,CSSOM 则必须等到所有字节收到才开始构建。

# 构建 CSSOM

CSSOM (CSS Object Model),翻译过来叫做 CSS 对象模型。它的构建过程类似 DOM,当 HTML 解析中遇到 <link> 标签时,会请求对应的 CSS 文件,当 CSS 文件就位时,便开始解析它(如果遇到行内 <style> 时则直接解析),这一解析过程可以和构建 DOM 同时进行。

假设有如下 CSS 代码:

body {
  font-size: 16px;
}

p {
  font-weight: bold;
}

span {
  color: red;
}

p span {
  display: none;
}

img {
  float: right;
}

构建出来的 CSSOM 是这样的:

需要注意的是,上面并不是一颗完整的 CSSOM 树,文档有一些默认的 CSS 样式,称作 user agent styles,上面只展示了我们覆盖的部分。

CSSOM 的构建必须要获得一份完整的 CSS 文件,而不像 DOM 的构建是一个循序渐进的过程。因为我们知道,CSS 文件中包含大量的样式,后面的样式会覆盖前面的样式,如果提前就构建 CSSOM,可能会得到错误的结果

# 构建 Render Tree

这也是关键的一步,浏览器使用 DOM 和 CSSOM 构建出 Render Tree。此时不像构建 DOM 一样把所有节点构建出来,浏览器只构建需要在屏幕上显示的部分,因此像 <head><meta> 这些标签就无需构建了。同时,对于 display: none 的元素,也无需构建。

display: none 告诉浏览器这个元素无需出现在 Render Tree 中,但是 visibility: hidden 只是隐藏了这个元素,但是元素还占空间,会影响到后面的 Layout 过程,因此仍然需要出现在 Render Tree 中。

构建过程遵循以下步骤:

  1. 浏览器从 DOM 树开始,遍历每一个“可见”节点
  2. 对于每一个"可见"节点,在 CSSOM 上找到匹配的样式并应用
  3. 生成 Render Tree

# 扩展:CSS 匹配规则为何从右向左

CSS 匹配就发生在 Render Tree 构建时(Chrome Dev Tools 里叫做 Recalculate Style),此时浏览器构建出了 DOM,而且拿到了 CSS 样式,此时要做的就是把样式跟 DOM 上的节点对应上,浏览器为了提高性能需要做的就是快速匹配。

要明确一点,浏览器此时是给一个"可见"节点找对应的规则,这和 jQuery 选择器不同,后者是使用一个规则去找对应的节点,这样从左到右或许更快。但是对于前者,由于 CSS 的庞大,一个 CSS 文件中或许有上千条规则,而且对于当前节点来说,大多数规则是匹配不上的,到此为止,稍微想一下就知道,如果从右开始匹配(也是从更精确的位置开始),能更快排除不合适的大部分节点,而如果从左开始,只有深入了才会发现匹配失败,如果大部分规则层级都比较深,就比较浪费资源了。

前面还提到 DOM 构建是"循序渐进的",而且 DOM 不阻塞 Render Tree 构建(只有 CSSOM 阻塞),这样也是为了能让页面更早有元素呈现。考虑如下情况,如果我们此时构建的只是部分 DOM,而此时 CSSOM 构建完成,浏览器此时需要构建 Render Tree,如果对每一个节点,找到一条规则进行从左向右匹配,则必须要求其子元素甚至孙子元素都在 DOM 上,但此时 DOM 未构建完成,显然会匹配失败。如果反过来,只需要查找该元素的父元素或祖先元素,它们肯定在当前的 DOM 中。

  • Layout

现在为止已经得到了所有元素的自身信息,但是还不知道它们相对于 Viewport 的位置和大小,Layout 这一过程需要计算的就是这两个信息。

根据这两个信息,Layout 输出元素的 Box Model。现在已经拿到了元素相对于 Viewport 的详细信息,所有的值都已经计算为相对 Viewport 的精确像素大小和位置,就差显示了。

  • Paint

浏览器将每一个节点以像素显示在屏幕上,最终看到页面。

这一过程需要的时间与文档大小,CSS 应用样式的多少以及复杂度,还有设备自身都有关,例如对简单的颜色进行 Paint 是简单的,但是对 box-shadow 进行 Paint 则是复杂的。

  • 引入 js

  • 解析 HTML 构建 DOM 时,遇到js会被阻塞

js执行会被 CSSOM 构建阻塞,也就是说,JavaScript 必须等到 CSS 解析完成后才会执行(这只针对在头部放置 <style> 和 <link> 的情况,如果放在尾部,浏览器刚开始会使用 User Agent Style 构建 CSSOM

如果使用异步脚本,脚本的网络请求优先级降低,且网络请求期间不阻塞 DOM 构建,直到请求完成才开始执行脚本

注意:不要混淆 DOM, CSSOM, Render Tree 这三个概念

<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="../css/main.css" />
    <title>Critical Rendering Path with separate script file</title>
  </head>

  <body>
    <p>What's up? <span>Bros. </span>My name is tianzhich</p>
    <div>
      <img src="../images/xiaoshuang.jpg" alt="小爽-流星雨" height="500" />
    </div>
    <script src="../js/main.js"></script>
  </body>
</html>
var span = document.getElementsByTagName("span")[0];
span.textContent = "Girls. "; // change DOM text content
span.style.display = "inline"; // change CSSOM property

// create a new element, style it, and append it to the DOM
var loadTime = document.createElement("div");
loadTime.textContent = "You loaded this page on: " + new Date();
loadTime.style.color = "blue";
document.body.appendChild(loadTime);
body {
  font-size: 16px;
}

p {
  font-weight: bold;
}

span {
  color: red;
}

p span {
  display: none;
}

img {
  float: right;
}

链接 (opens new window)

最后更新: 4/6/2022, 7:07:35 AM