CSS 性能优化
问题
CSS 如何影响页面性能?哪些 CSS 属性和写法会导致性能问题?如何优化?
答案
CSS 与渲染管线
浏览器的渲染管线:
CSS 性能优化的核心:减少每个阶段的工作量,跳过不必要的阶段。
选择器性能
选择器匹配方向
浏览器从右到左匹配选择器。对于 .nav ul li a:
- 先找所有
<a> - 过滤父元素是
<li>的 - 过滤祖先是
<ul>的 - 过滤祖先有
.nav的
/* ❌ 避免:层级深、范围广 */
div > ul > li > a > span { }
.wrapper * { }
/* ✅ 推荐:直接使用类名 */
.nav-link { }
.nav-link-icon { }
选择器效率排序
从快到慢:
- ID 选择器
#id - 类选择器
.class - 元素选择器
div - 属性选择器
[attr] - 通配符
* - 后代选择器
.a .b(最慢)
现代浏览器的选择器引擎已经高度优化,选择器性能差异通常可忽略。但在有上万 DOM 节点的复杂页面中,合理的选择器仍然有意义。
真正的 CSS 性能瓶颈更多在于重排重绘和合成层管理。
重排(Reflow/Layout)与重绘(Repaint)
触发重排的操作
重排会重新计算元素的几何属性,开销最大:
/* 触发重排的属性 */
width, height, min-height, max-width
margin, padding, border-width
display, position, float
top, left, right, bottom
font-size, font-weight, line-height
overflow, text-align, vertical-align
// JS 中触发重排的操作
element.offsetWidth; // 读取几何属性(强制同步布局)
element.clientHeight;
element.getBoundingClientRect();
window.getComputedStyle(element);
触发重绘但不重排
/* 只触发重绘 */
color, background-color
visibility, outline
box-shadow, border-radius
只触发合成(最高效)
/* 只触发合成 */
transform
opacity
will-change
减少重排重绘
1. 批量修改 DOM
// ❌ 多次触发重排
const el = document.querySelector('.box') as HTMLElement;
el.style.width = '100px'; // 重排
el.style.height = '100px'; // 重排
el.style.margin = '10px'; // 重排
// ✅ 批量修改
el.style.cssText = 'width: 100px; height: 100px; margin: 10px;';
// 或使用 className
el.className = 'box-modified';
2. 避免强制同步布局
// ❌ 强制同步布局(读写混合)
for (const el of elements) {
el.style.width = el.offsetWidth + 10 + 'px'; // 每次循环都触发强制重排
}
// ✅ 读写分离
const widths = elements.map(el => el.offsetWidth); // 批量读
elements.forEach((el, i) => {
el.style.width = widths[i] + 10 + 'px'; // 批量写
});
3. 使用 transform 代替几何属性
/* ❌ 触发 Layout */
.animate {
transition: left 0.3s, top 0.3s;
}
/* ✅ 只触发 Composite */
.animate {
transition: transform 0.3s;
}
contain(包含属性)
contain 告诉浏览器元素内部的变化不会影响外部,限制重排重绘的范围:
.widget {
contain: layout; /* 布局隔离 */
contain: paint; /* 绘制隔离 */
contain: size; /* 尺寸隔离 */
contain: style; /* 计数器/引号隔离 */
contain: content; /* = layout + paint(推荐) */
contain: strict; /* = layout + paint + size */
}
/* 列表中每个 item 独立包含 */
.list-item {
contain: content; /* item 内部变化不触发列表重排 */
}
/* 固定尺寸的小部件 */
.sidebar-widget {
contain: strict; /* 完全隔离,包括尺寸 */
width: 300px;
height: 200px;
}
content-visibility(内容可见性)
最强大的 CSS 性能优化属性之一。跳过屏幕外(不可见)元素的渲染:
.card {
content-visibility: auto;
/* 必须设置 contain-intrinsic-size,否则滚动条会跳动 */
contain-intrinsic-size: 0 200px; /* 预估高度 200px */
}
效果:
- 屏幕外的
.card直接跳过 Layout + Paint - 滚动到可视区域时才渲染
- 长列表性能提升可达 7-10 倍
- 必须配合
contain-intrinsic-size使用,否则滚动条高度不准确 - 不适用于需要精确定位的元素
- 可能导致页面内搜索(Ctrl+F)找不到内容(浏览器正在改善)
will-change
提前告知浏览器元素即将变化的属性:
/* 在动画前添加 */
.card:hover {
will-change: transform;
}
.card:hover .content {
transform: scale(1.05);
}
/* ❌ 错误用法:全局添加 */
* { will-change: transform, opacity; }
/* ❌ 错误用法:过多元素 */
.list-item { will-change: transform; } /* 1000 个 item = 1000 个合成层 */
will-change 会将元素提升到独立合成层,每个层占用 GPU 内存。过多合成层会导致:
- 内存飙升(每层都要分配纹理)
- 渲染变慢(层too多反而影响合成性能)
Chrome DevTools → Layers 面板可以查看合成层数量。
CSS 加载优化
关键 CSS 内联
<head>
<!-- 内联首屏关键 CSS -->
<style>
body { margin: 0; font-family: system-ui; }
.hero { height: 100vh; display: flex; align-items: center; }
</style>
<!-- 异步加载非关键 CSS -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
</head>
减少 CSS 体积
/* 使用简写属性 */
margin: 10px 20px 10px 20px; /* → */ margin: 10px 20px;
/* 避免冗余选择器 */
body div.container ul li a { } /* → */ .nav-link { }
避免 @import
/* ❌ 串行加载 */
@import url('reset.css');
@import url('theme.css');
/* ✅ 并行加载 */
<link rel="stylesheet" href="reset.css">
<link rel="stylesheet" href="theme.css">
字体优化
@font-face {
font-family: 'MyFont';
src: url('font.woff2') format('woff2');
font-display: swap; /* 先显示后备字体,加载完毕后切换 */
unicode-range: U+0020-007E; /* 只加载需要的字符范围 */
}
font-display 策略详见字体优化。
常见面试问题
Q1: 哪些 CSS 属性会触发重排?如何避免?
答案:
改变元素几何属性的操作会触发重排:width、height、margin、padding、position、display、font-size 等。
避免方法:
- 动画用
transform代替top/left/width - 批量修改 DOM,避免逐条操作
- 使用
contain限制重排范围 - 使用
documentFragment或离屏 DOM
Q2: content-visibility: auto 有什么用?
答案:
它让浏览器跳过屏幕外元素的渲染(包括布局和绘制),相当于内置的虚拟滚动。对长列表、长页面性能提升显著。
必须配合 contain-intrinsic-size 提供预估尺寸,否则滚动条会跳动不准确。
Q3: will-change 为什么不能滥用?
答案:
will-change 会将元素提升到独立合成层,每个层需要 GPU 内存。过多合成层会:
- 显存占用飙升(每个合成层需要分配纹理)
- 合成阶段变慢(GPU 需要合成大量层)
- 反而降低性能(层爆炸)
正确做法:只在即将发生动画时添加,动画结束后移除。
Q4: CSS 选择器性能重要吗?
答案:
现代浏览器的选择器解析已经非常高效,在普通页面中选择器性能差异基本可忽略。
但在以下场景仍需注意:
- 上万个 DOM 节点的复杂页面
- 大量使用通配符或深层后代选择器
- 频繁 DOM 更新导致样式重算
最佳实践:使用短而具体的类名选择器,避免不必要的嵌套。
Q5: 什么是关键 CSS?如何提取?
答案:
关键 CSS(Critical CSS)是首屏渲染所需的最小 CSS 集合。将其内联到 <head> 中可以消除 CSS 阻塞渲染的问题。
提取工具:
Q6: contain 属性有什么用?
答案:
contain 创建包含上下文,限制元素内部变化对外部的影响:
| 值 | 效果 |
|---|---|
layout | 内部布局变化不影响外部 |
paint | 内部绘制不溢出边界 |
size | 元素尺寸不依赖子元素 |
content | layout + paint(推荐) |
strict | layout + paint + size |
适合独立的 widget、卡片、列表项等组件。
Q7: 如何检测和排查 CSS 性能问题?
答案:
| 工具 | 用途 |
|---|---|
| Chrome DevTools → Performance | 查看 Recalculate Style、Layout、Paint 耗时 |
| Chrome DevTools → Rendering | 开启 Paint Flashing、Layout Shift Regions |
| Chrome DevTools → Layers | 查看合成层数量和内存占用 |
| Lighthouse | 综合性能评分和建议 |
| Coverage 面板 | 查看未使用的 CSS 比例 |
更多排查技巧参见常见性能问题与排查。