跳到主要内容

CSS 性能优化

问题

CSS 如何影响页面性能?哪些 CSS 属性和写法会导致性能问题?如何优化?

答案

CSS 与渲染管线

浏览器的渲染管线:

CSS 性能优化的核心:减少每个阶段的工作量,跳过不必要的阶段

选择器性能

选择器匹配方向

浏览器从右到左匹配选择器。对于 .nav ul li a

  1. 先找所有 <a>
  2. 过滤父元素是 <li>
  3. 过滤祖先是 <ul>
  4. 过滤祖先有 .nav
/* ❌ 避免:层级深、范围广 */
div > ul > li > a > span { }
.wrapper * { }

/* ✅ 推荐:直接使用类名 */
.nav-link { }
.nav-link-icon { }

选择器效率排序

从快到慢:

  1. ID 选择器 #id
  2. 类选择器 .class
  3. 元素选择器 div
  4. 属性选择器 [attr]
  5. 通配符 *
  6. 后代选择器 .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 性能优化属性之一。跳过屏幕外(不可见)元素的渲染:

content-visibility: auto
.card {
content-visibility: auto;
/* 必须设置 contain-intrinsic-size,否则滚动条会跳动 */
contain-intrinsic-size: 0 200px; /* 预估高度 200px */
}

效果:

  • 屏幕外的 .card 直接跳过 Layout + Paint
  • 滚动到可视区域时才渲染
  • 长列表性能提升可达 7-10 倍
content-visibility 注意事项
  • 必须配合 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 属性会触发重排?如何避免?

答案

改变元素几何属性的操作会触发重排:widthheightmarginpaddingpositiondisplayfont-size 等。

避免方法:

  1. 动画用 transform 代替 top/left/width
  2. 批量修改 DOM,避免逐条操作
  3. 使用 contain 限制重排范围
  4. 使用 documentFragment 或离屏 DOM

Q2: content-visibility: auto 有什么用?

答案

它让浏览器跳过屏幕外元素的渲染(包括布局和绘制),相当于内置的虚拟滚动。对长列表、长页面性能提升显著。

必须配合 contain-intrinsic-size 提供预估尺寸,否则滚动条会跳动不准确。

Q3: will-change 为什么不能滥用?

答案

will-change 会将元素提升到独立合成层,每个层需要 GPU 内存。过多合成层会:

  1. 显存占用飙升(每个合成层需要分配纹理)
  2. 合成阶段变慢(GPU 需要合成大量层)
  3. 反而降低性能(层爆炸)

正确做法:只在即将发生动画时添加,动画结束后移除。

Q4: CSS 选择器性能重要吗?

答案

现代浏览器的选择器解析已经非常高效,在普通页面中选择器性能差异基本可忽略

但在以下场景仍需注意:

  • 上万个 DOM 节点的复杂页面
  • 大量使用通配符或深层后代选择器
  • 频繁 DOM 更新导致样式重算

最佳实践:使用短而具体的类名选择器,避免不必要的嵌套。

Q5: 什么是关键 CSS?如何提取?

答案

关键 CSS(Critical CSS)是首屏渲染所需的最小 CSS 集合。将其内联到 <head> 中可以消除 CSS 阻塞渲染的问题。

提取工具:

  • critical — 自动提取
  • critters — Webpack 插件
  • Next.js / Nuxt 等框架自带关键 CSS 提取

Q6: contain 属性有什么用?

答案

contain 创建包含上下文,限制元素内部变化对外部的影响:

效果
layout内部布局变化不影响外部
paint内部绘制不溢出边界
size元素尺寸不依赖子元素
contentlayout + paint(推荐)
strictlayout + 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 比例

更多排查技巧参见常见性能问题与排查

相关链接