前言
其实最近在面一些公司的时候,很少在1面被问到一些css相关的问题,通常可能都是聊JS比较多,JS聊着聊着可能就聊到React上了,聊到浏览器上了,聊到性能优化上了。
不过,其实有时候你如果能够将CSS的知识结合到某些问题的回答当中,我想应该也会是加分项。
举个例子,比如聊到性能优化的时候,面试官问你性能优化有哪些方式,你可能会很自然的想到文件压缩,比如通过Webpack
等构建工具,使用相关的插件进行文件压缩:
安装css-minimizer-webpack-plugin
,然后在配置文件添加以下内容:
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = { // ...
optimization: {
minimize: isEnvProduction,
minimizer: [
// This is only used in production mode
new CssMinimizerPlugin(),
],
},
// ...
};
create-react-app
这个脚手架实际上就已经帮我们完成了很多通过基于构建工具配置的优化。
压缩后的文件体积更小,用户在访问网页时需要下载的数据量就减少了,从而加快了页面的加载速度。
那么,为什么页面的加载速度会变快呢?
这除了下载数据速度变快了之外,实际上解析文件的速度也变快了,从多方面提升了页面的加载速度。
这里其实就在考察你有关浏览器渲染页面的知识点了,如何解析css文件(词法分析、语法分析),从而构建CSSOM树等等。
css-minimizer-webpack-plugin
移除代码中的空格、换行符、注释等不必要的内容、简化CSS中的颜色代码、长度单位等,例如将 #FFFFFF
简化为 #FFF
,将 0.5px
简化为 .5px
、优化CSS选择器、移除未使用的CSS规则等等,从而进一步加快了CSS文件被解析时的速度。
OK、OK,现在我们回到本篇文章,前言的内容一不小心就说了很多,总而言之,准备CSS相关的知识点并不是一件性价比很低的事情,还是有着锦上添花的意义的。
就让我们一起来学一些干货吧!
CSS选择器
不是,2024年了,为什么还有人还在文章里去聊CSS选择器呢?炒冷饭大王,别再炒了吧......
好,很好,阁下的疑问我能理解,那么,在下抛出几个问题,阁下是否能自如应对呢?
- 在code review的时候,什么样的CSS代码写法是不推荐的,什么样的CSS代码写法是推荐的,推荐写法的好处有哪些?
!import
的作用是?为什么我们不建议频繁地使用!import
?- 什么情况下我们选择使用伪元素,伪元素的好处是什么,它能帮助性能优化吗?它和伪类的区别又是什么呢?
- 假设有一个列表
<ul>
,其中的偶数行和奇数行需要不同的背景色,你会如何编写CSS选择器来实现这一效果?
上述这些问题涵盖了性能优化、代码规范、实际业务等多个方面,如果阁下全部都能化解,那在下佩服,如果阁下还是中了几招,那么在下侥幸。
不扯淡了,我们就一起来看看为啥老掉牙的东西还要被讲一讲。
先从最热门的性能优化来说,聊到性能优化,往往离不开浏览器的渲染过程。
问:浏览器的性能都开销在哪里呢?
答:DOM
DOM的修改,比如变更了宽度、高度等空间位置信息,会导致浏览器发生回流(reflow),也就是引发了渲染树的改> 变,渲染树的改变会带来性能上的消耗,因此我们期望能够尽量避免回流的发生(重绘也是同理)。
看到这里,你可能会想,是的,我说的没错,但是这和CSS选择器又有什么关系呢?
我想说的是,我们也可以基于渲染流程去优化我们写的CSS。
浏览器渲染页面的过程
这里我们简单回顾一下浏览器的渲染流程。
当我们输入url之后,浏览器会进行如下几个步骤:
- 响应:首先,浏览器会向服务器发送HTTP请求,以获取对应的资源文件,通常是HTML文件。
- 解析:拿到资源之后,通过HTML解释器和CSS解释器将其转换成对应的DOM树和CSSOM树。
- 样式:接着,浏览器会将DOM树和CSSOM树合并,生成渲染树(Render Tree)。
- 布局:然后,从根节点开始递归调用,为渲染树中的每一个节点给出其在屏幕上出现的精确坐标,递归结束后,我们就得到了一颗带有布局信息的渲染树。
- 绘制:最后,浏览器使用UI后端层绘制每个节点,将内容显示在屏幕上。
(若对渲染的每一步期望加深理解,可以访问渲染页面:浏览器的工作原理 - Web 性能 | MDN (mozilla.org) )
从渲染流程优化页面性能
浏览器会将DOM树和CSSOM树合并,生成渲染树(Render Tree),换句话说,每当浏览器遇到一个新的DOM节点时,它都会通过CSS引擎去查询CSS样式表,找到适合应用在这个DOM节点上的样式规则,然后带着相关的样式信息,重新绘制这个DOM节点,让它成为渲染树的节点。
敏锐的你可能发现了,上述内容里提到了一个很容易导致性能问题的过程,没错,就是查询过程。
经常写算法题的同学可能会更有感受,当我们写一些中等题、困难题时,基本上给的用例的数据范围都不会允许我们使用暴力查询(比如O(N^2)
的时间复杂度)这种方式,是AC不了的。
我们该如何去写CSS文件,从而能够加快浏览器CSS引擎查询CSS样式表的速度呢?
俗话说得好,在跑之前,得先学会走路
在介绍一些优化的写法之前,我们得先了解CSS引擎到底是如何去解析CSS文件的。
我们来看一种写法:
.myContainer p {
text-align: center
}
上面这行代码的意思很好理解,给className是myContainer
下的所有<p>
标签设置文本内容居中。
根据我们的阅读习惯,会觉得这样写没有任何问题,先找到myContainer
然后再处理<p>
标签。
但是,对于CSS引擎查找样式表,并不是我们理想当然认为的那样,对于每条CSS规则,它是遵循从右往左的顺序去匹配的。
也就是说,回到刚刚我们写的CSS代码,
.myContainer p {
text-align: center
}
实际上浏览器的CSS引擎是怎么做的呢?
它会遍历页面上全部的
<p>
标签,然后去判断当前遍历的<p>
标签的父级元素的className是否是myContainer
,如果是,则给此<p>
标签设置text-align: center
。
我们当然不希望只是因为加了这样一条CSS规则,从而就得让浏览器把页面上所有的<p>
标签都遍历一遍,导致页面渲染速度下降。所以,我们要怎么去优化这种CSS代码的写法呢?
这就回到了本章节我一开始提出的问题了:
在code review的时候,什么样的CSS代码写法是不推荐的,什么样的CSS代码写法是推荐的,推荐写法的好处有哪些?
比如我们确实需要对某一个块级元素下包裹的子元素有特殊的样式规则要求,我们不应该通过类名 + 标签名
的形式去写这条CSS规则,而是应该直接给这个元素定义类名,比如:
.myContainer_p {
text-align: center
}
这样,浏览器就不会去遍历页面上全部的<p>
标签了,从而加快了CSS引擎查询CSS样式表的速度。
除此之外,还有其他几个方面也是我们写CSS文件时需要注意的:
-
尽量不使用通配选择器
*
,我们如果直接在MDN文档上阅读此选择器相关的内容,可以看到被特别标注的一条说明:The key to optimizing CSS selectors is to focus on the rightmost selector, also called the key selector (coincidence?). Here’s a much more expensive selector:
A.class0007 * {}
. Although this selector might look simpler, it’s more expensive for the browser to match. Because the browser moves right to left, it starts by checking all the elements that match the key selector, “*
“. This means the browser must try to match this selector against all elements in the page.——simplifying-css-selectors我只是高亮了几个短语,大家应该也能大致懂什么意思了,通配选择器
*
会让浏览器遍历当前页面的所有元素,天呐!这将导致多么恐怖的性能负优化.....比我们刚刚聊的那种写法有过之而无不及。 -
尽量少使用标签选择器,比如
<p>
、<span>
、<li>
,至于原因我们在一开始就讲过了,会导致浏览器遍历当前页面全部的标签元素。- 尽可能多使用类选择器,用类去关联每一个标签元素,比如刚刚举过的那个例子:
.myContainer_p { text-align: center }
-
减少嵌套的写法(最高不超过3层),使用深层嵌套的后代选择器,会极大增加浏览器的匹配成本。举个例子:
div.container > section.main-content > article.post > header.post-header > h1 { font-size: 24px; color: #333; }
对于这个例子来说,根据我们之前讲的从右往左,CSS引擎首先会遍历全部的
<h1>
标签,然后判断它的父元素是否是header.post-header
,然后再遍历全部的header.post-header
,判断它的父元素是否是article.post
,然后再遍历全部的....我就不一一赘述了。大家能够感受到后代选择器所带来的巨额开销了吧。因此利用之前的建议写法,我们将类去关联标签元素,可以优化上述的写法:
.post_title_h1 { font-size: 24px; color: #333; }
代码规范
然后我们继续看另一个在章节开头我提到的问题:
!import
的作用是?为什么我们不建议频繁地使用!import
?
一开始在公司干活的时候,我有时会给一些样式属性加上!import
,从而让这条样式属性能够生效,但是这样的代码通常会在code review
的时候被主管打回。
看着合并请求上一堆待修改的CR问题,我陷入了沉思,为什么不建议频繁地使用!import
呢?我开始思考几个问题:
- 我为什么要用
!import
,这个使用是必要的吗?这个使用可以被更优雅的解决方案替换掉吗? - 是我想不到更优雅的替换方案,比如选择器的优先级,还是说只是因为我懒?
- 如果是因为懒,这是不是一种对后续接手这个项目的同学不负责?毕竟遵循黄金法则,以你期望他人对待你的方式对待他人,推己及人,我肯定也不想接手别人的屎山代码。
!import
不合理在哪里?
选择器优先级
选择器 | 格式 | 优先级权重 |
---|---|---|
id 选择器 | #id | 100 |
类选择器 | .classname | 10 |
属性选择器 | input[type="text"] | 10 |
伪类选择器 | div:hover | 10 |
标签选择器 | div | 1 |
伪元素选择器 | span::before | 1 |
相邻兄弟选择器 | h1+p | 0 |
子选择器 | ul>li | 0 |
后代选择器 | li a | 0 |
通配符选择器 | * | 0 |
如果使用的选择器优先级都一样,则在样式表中靠后的规则会覆盖靠前的规则。
(在线DEMO)
不合理之处
- 维护困难:
!important
会使得CSS的调试和维护变得更加困难,因为它打破了正常的CSS优先级规则。当你需要修改样式时,可能需要查找并修改多个带有!important
的声明。 - 优先级混乱:过度使用
!important
会导致CSS文件中优先级混乱,使得其他开发者难以理解样式是如何被应用的。 - 代码冗余:为了覆盖
!important
,开发者可能不得不在CSS中添加更多的!important
声明,导致代码冗余。 - 灵活性降低:使用
!important
限制了样式的灵活性,因为某些样式被固定了,无法被后续的样式轻易覆盖。 - 性能影响:虽然
!important
本身不会直接影响页面加载的性能,但它可能会增加页面渲染的时间,因为浏览器需要处理更多的样式冲突。
伪元素与伪类
什么情况下我们选择使用伪元素,伪元素的好处是什么,它能帮助性能优化吗?它和伪类的区别又是什么呢?
首先,先回顾一下,什么是伪元素:
伪元素是一个附加至选择器末的关键词,允许你对被选择元素的特定部分修改样式。——MDN
比如,在某个页面,展示的文本内容都是英文,我期望每个段落的首字母能够以大写的形式呈现,并且以主题色高亮,此时就可以使用伪元素中的::first-letter
:
.es_p::first-letter {
color: brown;
text-transform: uppercase;
}
可以很方便的帮我们实现一些样式,比如在这里,就是加强了文字排版效果。(在线Demo)。
那么,使用伪元素的好处都有哪些呢?
-
减少DOM节点的数量:顾名思义,伪元素,前缀是一个“伪”字,使用伪元素可以在不增加HTML结构的情况下(即不会产生新的DOM节点)添加样式,从而减少页面的节点数。那么减少节点数量有哪些好处?
- 性能优化:我们都清楚,每个DOM节点都需要浏览器处理和渲染,因此替浏览器减少工作量,就能加快页面的加载速度和渲染性能。
- 内存占用减少:每个DOM节点都会占用内存。减少节点数可以降低页面的内存占用,从而改善整体性能。
-
提高维护性:由于样式和结构分离,更新和维护变得更加容易。
-
灵活性:可以很容易地通过CSS控制伪元素的显示和隐藏,而不需要操作DOM(在线Demo)。
而且从交互层面考虑,我们也可以使用伪元素来优化用户的使用体验。一些流行的组件库就是这么做的,比如ANTD 5.x版本下的一些Icon
组件。为了方便用户点击,给这些Icon组件加上了伪元素,扩大了点击区域的范围。
从上方的动图可以看到,鼠标在离“复制”按钮还有一部分间隔时,就已经能够触发Tooltip
了,方便用户完成点击操作。
感兴趣的同学可以阅读一下这篇文章: 【源码阅读】探索“一键复制文本”的最佳实践
根据上面聊的内容,我们可以概括一下伪元素的使用场景:
- 创建装饰性内容:比如给某一行文字前加上【重要提示】之类的内容。
- 增强排版效果:比如展示文本内容的时候,对文本的首行、首字母做一些样式处理。
- 优化交互体验:通过扩大一些小图片的可交互区域,优化用户的使用体验。
伪元素和伪类的区别
我个人认为最大的区别是两者扮演的角色不同,也就是功能定位上的不同。伪元素主要负责创建并插入内容,是带有附加性质的。而伪类则是根据元素的状态改变元素的样式。
最常见的伪类,比如:focus
、:hover
等等,都是代表该元素处于聚焦、悬浮状态时,应该有怎样的样式表现。
我们在MDN上阅读伪元素时,也可以看到编辑贴心的一份备注:
注意使用的方式,伪类是单冒号
:hover
,伪元素是双冒号::before
场景实战
经过了上面相关知识点的回顾与学习,接下来我们做一道业务上也可能会出现的题目,强化一下理论知识的掌握。
假设有一个列表
<ul>
,其中的偶数行和奇数行需要不同的背景色,你会如何编写CSS选择器来实现这一效果?
原生CSS
奇数行和偶数行,我们可以使用:nth-child()
伪类根据元素在父元素的子元素列表中的索引来选择元素。
/* 选择奇数行 */
ul li:nth-child(odd) {
background-color: #f9f9f9; /* 假设这是奇数行的背景色 */
}
/* 选择偶数行 */
ul li:nth-child(even) {
background-color: #e9e9e9; /* 假设这是偶数行的背景色 */
}
但是一开始我们说了,不建议使用标签选择器,期望能够将类与标签关联,因此我们也可以使用类选择器去实现这件事情:
.sec_ul .odd {
background-color: #007fff; /* 奇数行的背景色 */
}
.sec_ul .even {
background-color: #f0fdff; /* 偶数行的背景色 */
}
(不过这种方式不太适合动态数据,只适合数量确定时的写法,因为要给每一行标明className
)
CSS预处理器
在实际工作过程中,往往我们都会用一些CSS预处理器,比如LESS,因此这里我们也想一想用LESS实现的方案:
// 定义一个 Mixin 用于设置背景色
.set-background(@color) {
background-color: @color;
}
// 定义背景色变量
@odd-bg-color: #007fff; // 奇数行背景色
@even-bg-color: #f0fdff; // 偶数行背景色
ul {
li {
&:nth-child(odd) {
.set-background(@odd-bg-color);
}
&:nth-child(even) {
.set-background(@even-bg-color);
}
}
}
我们可以使用 Less 的 Mixins 来实现这个效果。Mixins 可以帮助我们更好地组织代码,并且可以复用样式
结语
在本篇文章中,我们通过一些不同的视角重新回顾了一遍CSS选择器相关的知识点,不知道这是否让你觉得有所收获呢?我相信应该能够在帮你回答类似的面试题时,扩展你答案的广度。
期待与你在下一篇文章相遇。