# 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 文件开始,到将它们最终以像素输出到屏幕上这一过程。包括以下几个部分:
- 构建 DOM
- 将 HTML 解析成许多 Tokens
- 将 Tokens 解析成 Objects
- 将 Objects 组合成为一个 DOM 树
- 构建 CSSOM
- 解析 CSS 文件,并构建出一个 CSSOM 树(过程类似于 DOM 构建)
- 构建 Render
- 结合 DOM 和 CSSOM 构建出一颗 Render 树
- Layout
- 计算出元素相对于 viewport 的相对位置
- 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 中。
构建过程遵循以下步骤:
- 浏览器从 DOM 树开始,遍历每一个“可见”节点
- 对于每一个"可见"节点,在 CSSOM 上找到匹配的样式并应用
- 生成 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;
}