样式不玄学,都有规则
CSS 面试最怕的就是"我知道怎么写,但说不清为什么"。这份把每条规则还原成浏览器实际在算什么:盒模型在算宽度、BFC 在算隔离、层叠上下文在算比较范围。
01先分清楚问的是哪一类居中、清除浮动这类问题,答案取决于前提,先问清前提再答。
02认准"谁在计算"宽度是包含块在算、z-index 是层叠上下文在比、margin 是相邻盒在合并。
03说出代价每种方案都有副作用,能讲出 flow-root 比 overflow 好在哪,才是真懂。
1
盒模型:box-sizing 改的到底是什么★
width 到底指谁
box-sizing 不改变计算方式,它改变的是 width 代表哪一段。content-box(默认)下 width 只算内容区,padding 和 border 往外加;border-box 下 width 就是最终占位宽度,内容区被反向压缩。◐ 可交互:拖 padding,看两种盒模型的实际宽度
border 5px
padding
content
↑ 按真实像素 1:1 绘制
CSS 里写的 width200px
实际占位宽度250px
内容区宽度200px
content-box 下 width 只管内容区,padding 和 border 往外加。
同样的 width: 200px,两种盒模型的实际宽度content-box(默认)
width 200 + padding 20*2 + border 5*2 = 250px 占位
内容区 = 200px
border-box
占位 = 200px
内容区 = 200 - 40 - 10 = 150px
断言:border-box 不会让盒子变大,只会让内容区变小。
所以"给元素加 padding 结果撑破了布局"这类问题,根源都是 content-box。
- 为什么默认不是 border-box:这是历史遗留。CSS1 时没有 box-sizing,后来为了兼容不能改默认值,于是 border-box 只能靠
* { box-sizing: border-box }手动全局打开。 - margin 在两种模型下都不计入 width:margin 永远是"盒子外面",所以
width: 100%加 margin 一定会溢出。 - 百分比宽度算的是包含块的 content-box 宽度,跟你自己的 box-sizing 无关。父元素如果是 border-box,子元素的 100% 对应的仍然是父元素的 content 区。
面试怎么答盒模型就是四个圈的嵌套:content、padding、border、margin。box-sizing 决定 width 属性指向哪一层。默认 content-box 指的是 content 层,所以 padding 和 border 会把盒子撑大;border-box 指的是 border 外沿,width 就是最终占位。工程里通常全局设成 border-box,写宽度时不用再心算 padding。
2
BFC:它解决的不是一个问题,是四个★
外边距塌陷只是其中最出名的一个
BFC(块级格式化上下文)是一个独立的布局容器:内部元素怎么排都影响不到外面。它能治四种病——父子外边距塌陷、父元素高度塌陷、被浮动元素覆盖、邻近元素 margin 合并。记住「隔离」这两个字,四种病都是同一件事的不同表现。
不加 BFC,两个经典 Bug
- 父子 margin 塌陷:子元素 margin-top 漏到父元素外面,父元素整体往下走
- 高度塌陷:子元素全浮动,父元素高度变成 0
- 文字被浮动元素盖住一半
- 两个相邻兄弟的 margin 取 max 而不是相加
加 BFC,四个都好了
- 父元素 overflow: hidden,子元素的 margin 被关在里面
- 浮动的子元素参与了父元素的高度计算
- 左边的浮动区自成一块,文字绕排变成并排
- 两个独立容器,margin 各自计算不合并
- 为什么 margin 会塌陷:相邻的垂直 margin 会合并成较大的那个,这是 CSS 规范里"相邻块级盒的外边距合并"规则。父子之间如果中间没有 border / padding / BFC 隔开,也算相邻。
- 推荐
display: flow-root:它就是为"我要一个 BFC 但不想有其他副作用"设计的,比 overflow: hidden 干净,不会裁掉阴影和溢出内容。 - overflow: hidden 的代价:会裁掉所有溢出内容,包括下拉菜单、tooltip、box-shadow。所以拿它当 BFC 用时要想清楚里面有没有往外冒的东西。
- flex 子项自带 BFC:所以 flex 布局里经常"这个问题怎么没了",其实是子项已经是独立格式化上下文。
面试怎么答BFC 是块级格式化上下文,本质是一个隔离的布局容器,内部布局不影响外部。它能解决四类问题:父子外边距塌陷、浮动导致的高度塌陷、元素被浮动覆盖、相邻 margin 合并。触发方式里最干净的是 display: flow-root,overflow: hidden 也常用但会裁掉溢出内容。
3
浮动与清除:高度塌陷的来路和三种解法
float 本来是给文字绕排用的
浮动元素脱离文档流但不脱离文本流——所以文字会绕着它排,而父元素算高度时又不认它。这就是高度塌陷。三种解法:父元素 BFC、clearfix 伪元素、clear 属性,现代项目基本只用前两种。
浮动元素脱离文档流不占普通流的位置,父元素算高度时跳过它
→
但仍在文本流里所以行内内容会绕开它排——这是 float 的设计初衷
→
父元素高度算不出来只剩 0,后面的兄弟元素顶上来被盖住
→
解法一:父元素 BFCflow-root / overflow:hidden,父元素就会包住浮动
→
解法二:clearfix伪元素 clear:both 把父元素撑到浮动元素下方
→
解法三:clear 属性在浮动后面加一个空元素 clear:both,多一层 DOM
- 为什么
display: table而不是block:伪元素设成 block 时,它自己也可能和上面元素产生 margin 合并;table 不会,所以老代码里都写 table。现在用 flow-root 就不需要这套了。 - clear 只能清同方向的浮动:
clear: left只避开左浮动。写clear: both最省事。 - 现在还用 float 吗:布局基本不用了(flex/grid 替代),但图片文字绕排、以及某些老组件库里还在用。
面试怎么答浮动的特点是脱离文档流但保留文本流,所以父元素算高度时不算它,就出现高度塌陷。解决方式三种:父元素触发 BFC 包住浮动、用 ::after 伪元素 clear: both 撑开、或者在浮动后加 clear 元素。现在推荐 display: flow-root,一行解决且没有副作用。
4
居中:八种写法,各有前提★
别背代码,先分清是哪一类居中
先问三个问题:水平还是垂直、单行还是多行、父容器尺寸知不知道。答完这三个,写法只有一个是对的。工程里 90% 的场景用 flex 居中就能覆盖,剩下的是绝对定位和 grid。
按前提选写法【父容器尺寸已知 + 子元素尺寸已知】
绝对定位 + 负 margin
position:absolute; top:50%; left:50%; margin:-h/2 0 0 -w/2
【尺寸未知】
绝对定位 + transform
position:absolute; top:50%; left:50%; transform:translate(-50%,-50%)
注意:translate 百分比是相对自身尺寸,所以不用知道宽高
【现代首选】
flex:display:flex; justify-content:center; align-items:center
grid:display:grid; place-items:center ← 一行,最简洁
【单行文字垂直居中】
line-height = 容器高度(多行会炸)
【块级元素水平居中】
margin: 0 auto(必须有明确 width,否则块级本来就占满)
【表格单元格法】
display:table-cell; vertical-align:middle(老方案,父元素还得是 table)
- 为什么
margin: 0 auto对行内元素无效:auto 是在"块级盒子剩余空间"上分配的,行内元素的宽度由内容决定,没有剩余空间可分配。 - 为什么 absolute + transform 不需要知道尺寸:
translate(-50%, -50%)里的百分比是相对元素自身算的,浏览器会自己量。而left: 50%的百分比是相对父容器。两个 50% 参照物不同,这正是它能居中的原因。 - flex 居中的坑:如果子元素内容超长,
align-items: center会把它往上顶出可视区,滚不到顶部。解决是给子元素加margin: auto或者容器加overflow: auto。 - grid 的 place-items: center 最省事:一行同时管水平和垂直,不需要额外容器属性。
面试怎么答居中先分类:水平居中,块级用 margin: 0 auto 但要给宽度,行内用 text-align。垂直居中,单行文字用 line-height 等于高度,多行就别用。通用场景用 flex 的 justify-content 加 align-items,或者 grid 的 place-items: center,一行就够。绝对定位加 transform: translate(-50%,-50%) 适合脱离文档流的弹窗,好处是不用知道自身宽高,因为 translate 的百分比是相对自己算的。
5
flex:flex: 1 到底等于什么★
一个简写藏着三个属性
flex: 1 展开是 flex: 1 1 0%——grow 1、shrink 1、basis 0%。第三个数才是关键:basis 为 0 意味着"宽度完全按剩余空间分配",而不是"在我原本宽度的基础上伸缩"。flex:1 和 flex:auto 的实际差别容器 600px,两个子项原始宽度分别是 100px 和 200px
flex: 1 (basis 0%) 每个分 300px ← 完全均分
flex: auto(basis auto) 100+500/3=266.7 ← 按原始宽度加增量
200+500/3=366.7
注意比例被保留了
结论:想均分用 flex:1,想按内容比例放大用 flex:auto。
- min-width: auto 是个陷阱:flex 子项的默认
min-width不是 0 而是 auto,意味着"不能小于内容的最小宽度"。所以一行文字很长的子项会拒绝收缩,把整个 flex 布局撑破,省略号也不生效。手动写min-width: 0才治得好。 - flex-basis 和 width 谁优先:都写时 basis 生效。basis 是"主轴上的初始尺寸",width 是兜底。
- flex 1 1 0 和 1 1 0% 的差别:
0是 0px,0%是相对容器宽度的 0%,实际都等于 0。规范建议写 0%,因为有些老浏览器对无单位 0 处理不一致。 - flex-wrap 之后的 align-content:换行后要控制多行的整体对齐用 align-content,单行内的对齐用 align-items,两个容易混。
面试怎么答flex 是三个属性的简写:grow、shrink、basis。flex: 1 展开是 1 1 0%,basis 为 0 表示从零开始分配剩余空间,所以表现是均分。flex: auto 是 1 1 auto,会保留原始宽度再放大。最常见的坑是子元素不肯收缩、省略号失效,原因是 flex 子项默认 min-width 是 auto,必须显式设成 0。
6
grid:二维布局该用它
flex 管一维,grid 管二维
flex 是一维(一行或一列内怎么排),grid 是二维(行列同时约束)。判断标准很简单:需要"上下两端也对齐"就用 grid。flex 要模拟等宽多行,得靠换行加固定宽度,很别扭。
auto-fill 与 auto-fit 的区别(高频)容器宽 900px,每列最小 220px
repeat(auto-fill, minmax(220px, 1fr))
放下 4 列 → 900/4 = 225px
余下空间也是 0,但因为 auto-fill 会"填满轨道",
如果只有 2 个元素,另外 2 个空轨道仍占位 → 元素不会拉伸
repeat(auto-fit, minmax(220px, 1fr))
轨道数量按需收缩 → 只有 2 个元素时,它们各占一半宽度
记忆:fill = 补空轨道,fit = 折叠空轨道
1fr不是百分比:fr 是"剩余空间的一份",会把 gap 先扣掉再分配。50% 50%加 gap 会溢出,1fr 1fr不会。这是 grid 比百分比好用的核心原因。- 隐式网格:元素超出定义的行数时会自动生成新行,由
grid-auto-rows控制高度。不设的话这些行高度由内容决定,容易长短不齐。 - grid-template-areas 只能矩形:区域必须连成矩形,L 形排不出来。这时得用
grid-column / grid-row手动指定起止线。
面试怎么答flex 是一维布局,只关心主轴上的排列;grid 是二维,行列同时定义。需要多行多列且行列都要对齐时用 grid,比如卡片墙、整页骨架。grid 的两个高频点:一是 1fr 会自动扣掉 gap 再分剩余空间,所以比百分比加 gap 更稳;二是 repeat(auto-fill) 和 auto-fit 的区别,前者保留空轨道,后者折叠空轨道让元素拉伸。
7
层叠上下文:z-index 为什么不起作用★
z-index 只在同一个层叠上下文里比
z-index 不是一个全局的天梯榜,它只在同一个层叠上下文内部比较。父元素一旦形成了新的层叠上下文,子元素再大的 z-index 也出不去——因为整个父元素作为一个整体参与上一层的比较。- 会创建层叠上下文的常见属性:
position非 static 且 z-index 非 auto、opacity小于 1、transform/filter/perspective非 none、will-change指定了这些属性、isolation: isolate、contain: paint、flex / grid 子项且 z-index 非 auto。 - 为什么加个 opacity 动画就盖不住了:淡入动画常写
opacity: 0 → 1,过程中 opacity 小于 1,每秒都在创建层叠上下文,所以 z-index 时灵时不灵。 - z-index 只对定位元素生效:给 static 元素写 z-index 完全无效——除非它是 flex / grid 的子项。
- 排查顺序:先看元素和要盖住的目标是不是同一个层叠上下文,再往上看祖先有没有创建新的上下文。DevTools 里 3D 视图能直接看出来。
面试怎么答z-index 只在同一个层叠上下文内比较,所以调的其实是兄弟之间的关系。如果父元素因为 opacity、transform、filter 这些属性创建了新的层叠上下文,子元素再大的 z-index 也超不出父元素这一层。排查方法是往上找祖先,看谁创建了层叠上下文,然后把 z-index 提到那一层去比,或者用 isolation: isolate 明确控制。
8
选择器优先级:权重怎么算
四个层级,不是一个数字
优先级是四元组 (行内, ID, class/属性/伪类, 元素/伪元素),从左往右逐级比较,不进位——11 个 class 也赢不过 1 个 ID。比较时只看选择器本身,跟元素在 DOM 里的位置无关。
权重计算示例 a b c d
* 0 0 0 0
li 0 0 0 1
ul li 0 0 0 2
.class 0 0 1 0
li.class 0 0 1 1
li.class:hover 0 0 2 1 ← 伪类算 c
#id 0 1 0 0
#id .class 0 1 1 0
style="..." - - - - ← 行内直接赢(除 !important)
!important ↑ 最高,仅次于浏览器默认的 important
比较:先比 a,a 大就赢;a 相同比 b,依此类推。
11 个 class = (0,0,11,0),输给 (0,1,0,0) 的 1 个 ID。
- :not() / :is() / :has() 不参与计算,取参数里最高的:
:not(#id)的权重等于#id,但:not自己不加分。 - :where() 权重恒为 0:这是它存在的唯一理由——写基础样式库时用它,用的人可以轻松覆盖。
- 继承的样式优先级最低:不是"0 分",而是"连比较的资格都没有",任何直接命中元素的规则都能盖掉继承值,哪怕那条规则权重是 0,0,0,1。
- 同权重看谁在后面:完全相同时,后声明的赢。所以引入顺序也会影响结果,这就是为什么说"少用 !important,多控加载顺序"。
- !important 之后怎么覆盖:只能再用一个 !important 且权重更高,或者更靠后。这是它被叫作"技术债"的原因——一旦用了就很难收回。
面试怎么答优先级是行内、ID、类选择器、元素选择器四段,逐段比较且不进位,所以 11 个 class 也输给 1 个 ID。伪类算第三段,伪元素算第四段。:where() 权重恒为 0,用来写可覆盖的基础样式;:is() 和 :not() 取参数里最高的那个。同权重时后声明的生效,所以引入顺序也会影响结果。
9
伪类与伪元素:单冒号还是双冒号
一个冒号是状态,两个冒号是造元素
伪类(:hover / :nth-child)描述元素的状态或位置,元素本身存在;伪元素(::before / ::first-line)凭空造出一个不在 DOM 里的部分。规范上伪元素用双冒号,伪类用单冒号;为了兼容 IE8 以前,
:before 也常见,等价于 ::before。:nth-child 和 :nth-of-type 的区别(高频)<div>
<p>1</p>
<span>2</span>
<p>3</p>
</div>
p:nth-child(2) → 不选中任何元素
第 2 个子元素是 span,不是 p
p:nth-of-type(2) → 选中 <p>3</p>
在"所有 p"里数第 2 个
一句话:child 按"所有兄弟"排队,of-type 按"同标签"排队。
- ::before 不是"在元素前面插入",而是变成元素的第一个子节点。所以它不能用在外层没有内容的替换元素上,比如
<img>、<input>。 - content 里可以放属性:
content: attr(data-tip),不用额外 DOM 就能做 tooltip 文字。 - ::selection 改选中文字的颜色,::placeholder 改占位符样式,这两个是高频实用伪元素。
- :focus-visible 比 :focus 好:鼠标点击不显示焦点环,键盘 Tab 才显示。无障碍和美观兼顾。
面试怎么答伪类描述状态或位置,元素本身在 DOM 里存在,比如 hover、focus、nth-child;伪元素是凭空造出来的部分,不在 DOM 里,比如 before、after、first-line,所以必须写 content 才生效。规范上伪元素用双冒号。常考的区别是 nth-child 按所有兄弟元素排队,nth-of-type 只按同标签排队。
10
移动端适配:viewport、rem、vw 与 1px★
三个问题混在一起,要拆开答
移动端适配其实是三件事:布局宽度怎么算(viewport + 弹性单位)、字号怎么缩放(rem / vw)、物理像素怎么画细线(1px 问题)。面试里最常被追问的是第三个——为什么 1px 在高清屏上会变粗。
三种适配方案对比【rem 方案】html 字号随屏幕宽度变化,所有尺寸用 rem
html { font-size: calc(100vw / 375 * 16); } /* 以 375 设计稿为基准 */
优点:兼容老浏览器,字号可整体缩放
缺点:需要 postcss 自动换算;字体缩放在某些场景下体验差
【vw 方案】
1vw = 屏幕宽度 1%,直接写 22.5vw 表示 375 稿上的 84.375px
优点:不需要 JS,纯 CSS,现在的主流
缺点:极端宽屏下字会很大,需要 max-width 兜底
【响应式 + 弹性布局(推荐组合)】
布局用 flex / grid + 百分比、min()/max()/clamp()
字号用 clamp(14px, 4vw, 18px) 限制上下界
只对特殊元素用 vw
- 1px 边框为什么变粗:CSS 里的 1px 是逻辑像素。DPR = 2 时,1 逻辑像素 = 2 物理像素,视觉上就粗一倍。三条解法:①
transform: scaleY(.5)伪元素画线;②box-shadow: 0 0 0 .5px;③border: 1px solid配合@media (-webkit-min-device-pixel-ratio: 2)换成 0.5px(iOS 支持,安卓部分不支持)。最稳的是伪元素 + transform。 - 为什么不用
initial-scale=0.5解决 1px:那会让整个页面缩小,字号和布局全乱。它解决的是另一个时代的问题(固定 750px 设计稿)。 - 安全区:iPhone 刘海屏底部横条会挡住固定定位的按钮,用
padding-bottom: env(safe-area-inset-bottom)让开,前提是 viewport 里写了viewport-fit=cover。 - 100vh 在移动端不可靠:浏览器地址栏收起时 vh 不变,页面会出现滚动。用
100dvh(动态视口高度)更准,或者用 JS 设 CSS 变量。
面试怎么答移动端适配拆成三点答。布局上通过 viewport 的 width=device-width 让布局视口等于设备宽度,再用 flex、grid、百分比做弹性布局。尺寸单位上,rem 方案是动态改 html 字号,vw 方案是直接用视口百分比,现在更推荐 vw 配合 clamp 限制上下界。1px 问题是因为 CSS 像素是逻辑像素,DPR 为 2 时 1px 会渲染成 2 个物理像素,所以看起来粗,最稳的解法是伪元素加 transform: scaleY(0.5)。
11
响应式与媒体查询
移动优先不是口号,是写法顺序
移动优先的意思是:基础样式写给手机,然后用
min-width 一层层往上加。反过来用 max-width 叫"优雅降级",两者不能混着写,否则断点会互相打架。- 为什么移动优先更好:手机性能弱、屏幕小,基础样式越简单越好;桌面端做增强。而且
min-width是叠加的,后写的覆盖前面的,规则简单。 - 断点选多少:不要照搬 Bootstrap。看自己的用户数据,或者干脆用内容驱动——把浏览器拉窄,布局开始难看的那一点就是断点。
hover: none很重要:触屏设备上 hover 会"粘住",第一次点触发 hover,第二次点才真的点击。所以依赖 hover 展示的菜单在手机上会很难用。- 容器查询:
@container按父容器宽度而不是视口宽度响应,做可复用组件时比媒体查询正确得多——同一个卡片放在侧栏和主区域应该长得不一样。
面试怎么答响应式布局按移动优先写:默认样式是最窄屏的,再用 min-width 媒体查询逐级增强,这样规则是叠加的,不会互相覆盖。断点不照搬框架,用内容驱动。另外按能力查询比按宽度查询更可靠,比如 prefers-color-scheme 做暗色、prefers-reduced-motion 关动效、hover: none 处理触屏没有悬停的问题。组件级别用容器查询比媒体查询更准确。
12
CSS 变量:运行时才生效的那一层
和 Sass 变量完全不是一回事
CSS 变量(自定义属性)是运行时的,能被子元素继承、能被 JS 改、能参与计算;Sass 变量是编译时的,编译完就没了。这条差别决定了主题切换、动态尺寸这些能力只有 CSS 变量能做。
CSS 变量 vs Sass 变量 CSS 变量 Sass 变量
生效时机 运行时 编译时
能否被 JS 改 能 不能(已展开)
能否被子元素覆盖 能,继承链 不能
能否用于媒体查询 不能(值没法在
查询条件里用) 能
能否参与 calc 能 能
主题切换 改一处即可 要重新编译
关键:CSS 变量存的是"字符串",浏览器在用到的地方才解析。
所以 var(--x) 里放 12px 或 red 都行,不做类型检查。
- 变量不能用在媒体查询条件里:
@media (min-width: var(--bp))无效,因为媒体查询在变量解析之前就算完了。这是最常见的误解。 - 变量在
:root上定义,但作用域是继承树:在某个组件根节点重新定义同名变量,就实现了局部覆盖,不用改任何规则。 - 没有值的变量会让整条声明失效:
color: var(--undefined)会被当作非法值丢弃,不会报错但也不生效。所以要么给兜底值,要么确保一定定义了。 - JS 读写用
setProperty/getPropertyValue:el.style.setProperty('--x', '20px')。这是"用 JS 控主题"的标准做法,比切 class 更细粒度。
面试怎么答CSS 变量是运行时的自定义属性,存在继承链上,能被 JS 读写,也能参与 calc,所以主题切换只要在一个容器上改值。Sass 变量是编译时的,编译完就被替换掉了,没法动态改。要注意 CSS 变量不能用在媒体查询的条件里,因为媒体查询在变量解析之前就已经计算完成。
13
CSS 工程化:命名冲突怎么真正解决
四代方案,越往后隔离越强
核心问题只有一个:样式是全局的。四代方案是逐步加强隔离:BEM 命名约定 → CSS Modules 编译期改类名 → CSS-in-JS 运行时注入 → 原子化(Tailwind)。不是越新越好,看团队和项目规模。
四代方案的本质区别【BEM】靠人守规矩,类名全局但命名唯一
.card__title--large { }
缺点:靠自觉;改一个类名要全局搜
【CSS Modules】编译期把类名哈希化,作用域天然隔离
.title { } → .title_a1b2c { }
缺点:动态样式不方便;跨组件复用要 compose
【CSS-in-JS】运行时/编译期生成样式,样式跟组件走
styled.div`color: ${p => p.color}`
优点:能读 props,动态能力最强
缺点:运行时开销、SSR 抽取样式麻烦、产物体积
(现在主流转向编译期的 vanilla-extract / styleX)
【原子化】不写新 CSS,只用预定义的原子类组合
<div class="p-4 rounded-lg bg-white shadow-sm">
优点:产物可复用、不会膨胀、没有命名问题
缺点:HTML 变长;复杂样式表达力弱;需要团队习惯
- scoped 是怎么实现的:Vue 的
<style scoped>给每个元素加data-v-xxx属性,选择器自动拼成.title[data-v-xxx]。它不会隔离子组件根节点以外的东西,所以:deep()才存在。 - Shadow DOM 才是真隔离:样式完全不出入,连继承都有限。代价是外部无法调整样式(只能通过 CSS 变量),所以组件库用它要谨慎。
- 原子化的产物为什么小:Tailwind 只生成用到的类,且同一个类在所有组件间复用。传统写法里
.btn { padding: 8px 16px }写十遍就是十份重复 CSS。 - 删除样式怎么判断:CSS 的死代码检测很难,因为类名可能是 JS 拼出来的。原子化方案天然没有这个问题,这也是它流行的一个隐性原因。
面试怎么答CSS 工程化要解决的核心是全局作用域和命名冲突。演进路线是 BEM 这种命名约定,到 CSS Modules 编译期哈希类名做隔离,再到 CSS-in-JS 让样式跟着组件走、能读 props,最后到 Tailwind 这种原子化方案,产物可复用、不膨胀。Vue 的 scoped 是加 data 属性做隔离,Shadow DOM 才是真正彻底的隔离。
14
动画:只动 transform 和 opacity★
为什么这两个属性便宜
浏览器渲染分布局、绘制、合成三步。
width / top 会触发布局,background / box-shadow 触发绘制,只有 transform 和 opacity 可以跳过前两步直接合成——因为合成器线程能独立完成。属性 → 触发哪些阶段触发 布局 + 绘制 + 合成
width / height / padding / margin / top / left / font-size
→ 每帧重排整棵相关子树,最贵
触发 绘制 + 合成
background-color / color / box-shadow / border-radius / visibility
只触发 合成
transform / opacity / filter(部分)
→ 合成器线程独立完成,不占用主线程
注意:transform 里用百分比也会触发一次布局计算来求参考尺寸,
所以高频动画优先写固定像素值或 translate3d。
- will-change 不能随便加:每个合成层都要占显存,加多了反而更慢,还可能触发层爆炸。原则是"动画开始前加上,结束后移除",或者只给真正在动的元素加。
- 为什么
top比transform贵:top 改变的是布局属性,浏览器要重新算这个元素以及受它影响的所有元素的位置。transform 只改变合成阶段的矩阵。 - 动画卡不一定是 CSS 的问题:主线程被长任务占住时,即使只动 transform 也会掉帧,因为合成层的更新也要排队。这种要看 Performance 面板里的长任务。
- 减少动态效果:
prefers-reduced-motion是为前庭功能敏感用户准备的,越来越多产品会做。写一条媒体查询成本很低。
面试怎么答浏览器渲染分布局、绘制、合成三步。改 width、top、margin 这类几何属性会触发重排,是最贵的;改颜色、阴影触发重绘;只有 transform 和 opacity 能跳过前两步,直接在合成器线程完成,所以流畅。需要动画的元素优先用 transform 位移和缩放,不用 top 和 left。will-change 能提前提升合成层,但会占显存,不能滥用。
15
语义化与 defer / async
这两件事经常被问到一块
语义化的实际收益不是"好看",而是无障碍、SEO、以及浏览器免费给你的交互(按钮能键盘触发、label 能关联输入框)。
defer 和 async 解决的是另一个问题:脚本下载和执行要不要卡住 HTML 解析。三种脚本加载的时序差别普通 <script>
解析 HTML ──[下载]──[执行]── 继续解析
下载和执行都阻塞解析,且放在 head 里会白屏
async
解析 HTML ──────────── 继续解析
└─[下载]─[随时执行] ← 执行时仍会阻塞解析
多个 async 之间顺序不保证
defer
解析 HTML ─────────────────────── 解析完
└─[下载]─┘ └─[按序执行]
只在 DOMContentLoaded 之前执行,顺序保证
结论:业务代码用 defer,独立统计脚本用 async。
- 为什么 alt 不是可选项:图片加载失败时的替代文字、屏幕阅读器读的内容、SEO 抓取的内容,三件事都靠它。纯装饰图应该写
alt=""明确表示"跳过"。 - button 和 div 的区别不只是默认样式:button 自带键盘可聚焦、回车空格触发、disabled 语义、表单提交角色。用 div 要补 tabindex、role、keydown,很容易漏。
- width / height 写死的意义:给图片预留比例,避免加载完页面跳动。这是 CLS 指标最直接的优化手段,比任何 JS 都有效。
- Defer 和 DOMContentLoaded 的关系:defer 脚本一定在 DOMContentLoaded 之前执行完,所以 defer 脚本里可以直接操作 DOM。async 脚本可能在 DOMContentLoaded 前也可能后。
面试怎么答语义化的收益在无障碍、SEO 和浏览器自带的交互行为,比如 button 天然可聚焦、可用键盘触发,label 能关联输入框。defer 和 async 都是并行下载,区别在执行时机:defer 等 HTML 解析完按顺序执行,保证依赖顺序,适合业务代码;async 下载完立刻执行,顺序不保证,适合独立的统计脚本。
16
Web Components 与 Shadow DOM★
真隔离,代价也真的大
Web Components 是四个浏览器原生能力的组合:Custom Elements(自定义标签)、Shadow DOM(样式和 DOM 隔离)、HTML Template(可复用的片段)、ES Module(分发方式)。它最大的卖点是框架无关——同一个组件能在 React、Vue 甚至纯 HTML 里用。
Shadow DOM 隔离了什么,没隔离什么隔离了
外部选择器进不来(.card { color: red } 影响不到里面)
内部的样式出不去
document.querySelector 拿不到 shadowRoot 里的节点
没隔离
CSS 继承属性仍会穿透(font-family、color 会继承进去)
CSS 自定义属性(--x)可以穿透,这是官方留的主题接口
事件会穿透(composed: true 的事件能冒泡到外面)
所以组件库用 Shadow DOM 时,主题化必须走 CSS 变量,
不能指望外部写个类名改样式。
- 为什么微前端和小程序都要提它:qiankun 的样式隔离灵感就来自这里;小程序的同层渲染、以及部分自定义组件的实现也用类似思路。所以这节不算冷门。
- React 用 Web Components 要注意:React 19 之前,自定义元素上的属性传值只支持字符串,对象和函数传不进去(会被当成 attribute)。要么用 ref 手动设,要么包一层。
onXxx事件监听也不生效,得addEventListener。 - SSR 不支持:Shadow DOM 是浏览器 API,服务端渲染时没有,需要判断环境或者只做客户端渲染。这是它没在业务项目普及的主要原因。
- 什么时候值得用:跨框架复用的组件库、需要嵌入第三方页面的挂件、以及不信任外部样式的场景。普通业务项目里,收益不如直接用框架组件。
面试怎么答Web Components 是 Custom Elements、Shadow DOM、Template、ES Module 四件套,最大价值是框架无关。Shadow DOM 提供真正的样式隔离,外部选择器进不去、内部出不来,但 CSS 继承属性和自定义属性是能穿透的,所以主题化要走 CSS 变量。代价是 SSR 不支持、和 React 的属性传递有兼容问题,所以更适合跨框架的组件库而不是普通业务。
17
速答清单
考前十分钟只看这一页
01box-sizing 改了什么?
改的是 width 指向哪一层。content-box 下 width 只算内容区,padding 和 border 往外加;border-box 下 width 是最终占位宽度,内容区被反向压缩。
02margin 会参与 width 计算吗?
不会。margin 永远在盒子外面,所以 width: 100% 再加 margin 一定会溢出。
03BFC 能解决哪四类问题?
父子外边距塌陷、浮动导致的高度塌陷、元素被浮动覆盖、相邻元素 margin 合并。本质都是"隔离"。
04最干净的 BFC 触发方式?
display: flow-root。没有 overflow: hidden 会裁剪溢出内容的副作用。
05三栏布局现在怎么做?
grid 的 grid-template-columns 一行搞定。老方案是浮动 + 负 margin(圣杯)或双飞翼,核心差别是中间栏有没有多包一层。
06水平垂直居中,最推荐哪种?
grid 的 place-items: center,一行同时管两个方向。弹窗类脱离文档流的用 absolute + transform: translate(-50%,-50%)。
07flex: 1 展开是什么?
flex: 1 1 0%。第三个值 basis 为 0,表示从零开始分配剩余空间,所以表现是均分。flex: auto 是 1 1 auto,保留原始宽度再放大。
08flex 子项不肯收缩、省略号失效怎么修?
给子项加 min-width: 0。默认值是 auto,意思是不能小于内容最小宽度。
09z-index 为什么不生效?
大概率父元素创建了新的层叠上下文(opacity、transform、filter、will-change 等),子元素的 z-index 出不去这一层。
10优先级 11 个 class 能赢 1 个 ID 吗?
不能。优先级是四段逐级比较,不进位。
11nth-child 和 nth-of-type 的区别?
child 按所有兄弟元素排队,of-type 只按同标签排队。
121px 边框为什么在高清屏变粗?
CSS 像素是逻辑像素,DPR 为 2 时 1px 渲染成 2 个物理像素。解法是伪元素 + transform: scaleY(0.5),最稳。
13CSS 变量能用在媒体查询里吗?
不能。媒体查询在变量解析之前就计算完成,所以 @media (min-width: var(--bp)) 无效。
14为什么动画只用 transform 和 opacity?
渲染分布局、绘制、合成三步。这两个属性可以跳过前两步,在合成器线程独立完成,不占主线程。
15defer 和 async 的区别?
都并行下载。defer 等 HTML 解析完按顺序执行,保证依赖顺序;async 下载完立刻执行,顺序不保证。业务代码用 defer。
16Shadow DOM 隔离了什么?
外部选择器进不来、内部样式出不去,但 CSS 继承属性和自定义属性仍能穿透,所以主题化要走 CSS 变量。