CSS渲染与优化
# 07.CSS渲染与优化
浏览器渲染流水线深度剖析(重排/重绘/合成)、CSS containment 四种模式、字体加载策略与 FOIT/FOUT、关键 CSS 提取实战、CSS-in-JS 性能权衡。
# 1. 浏览器渲染流水线
浏览器把 HTML+CSS 变成屏幕像素,历经五个阶段:
渲染流水线(每个阶段都可能成为性能瓶颈):
JavaScript → Style → Layout → Paint → Composite
│ │ │ │ │
│ 计算每个元素 计算每个元素 填充像素 合成图层
│ 的最终样式 的几何位置 输出到屏幕
| 阶段 | 触发条件 | 代价 |
|---|---|---|
| Layout(重排) | 修改几何属性(width/height/padding/margin/left/top) | 极高,级联更新整个子树 |
| Paint(重绘) | 修改视觉属性(color/background/box-shadow) | 中高 |
| Composite(合成) | 仅修改 transform/opacity | 极低,GPU 单独处理 |
# 1.1 触发重排的操作(尽量避免)
改几何属性:width, height, padding, margin, border
改位置属性:left, right, top, bottom, position
改文档流属性:display, float, overflow
读布局属性:offsetWidth, offsetHeight, getBoundingClientRect()
→ 读之前若有待处理的样式变更,浏览器会强制同步重排
# 1.2 触发重绘的操作
color, background, background-image, box-shadow,
border-color, outline, visibility(≠ hidden)
# 1.3 批量 DOM 修改技巧
// ❌ 每次读/改都触发重排
for (let i = 0; i < 100; i++) {
el.style.width = i + 'px';
console.log(el.offsetWidth); // 强制同步重排 × 100
}
// ✅ 读写分离
const widths = [];
for (let i = 0; i < 100; i++) widths.push(el.offsetWidth); // 仅读
for (let i = 0; i < 100; i++) el.style.width = widths[i]; // 仅改
// ✅ 或使用 requestAnimationFrame 批量
requestAnimationFrame(() => {
el.classList.add('expanded'); // 一次重排
});
# 2. CSS containment — 隔离渲染影响
contain 属性告诉浏览器"此元素的内部变化不会影响外部",从而跳过不必要的重排重绘:
contain 的值(可组合):
contain: layout → 内部布局不影响外部(最常用)
contain: paint → 内部绘制不超出边界(配合 overflow: hidden)
contain: size → 元素尺寸不依赖子元素(需显式设宽高)
contain: style → 计数器/引用等不影响外部(很少单独用)
contain: content → = layout + paint
contain: strict → = layout + paint + size
/* 列表项容器:内部不会影响外部布局 */
.list-container {
contain: layout;
}
/* 可滚动区域:内容裁剪,不触发外部重绘 */
.scroll-area {
overflow: auto;
contain: paint;
}
/* 固定尺寸组件 */
.widget {
width: 300px; height: 200px;
contain: strict; /* 完整隔离,最大性能收益 */
}
# 3. 关键 CSS 提取
Critical CSS = 首屏可见内容需要的 CSS。提取后内联到 <head>,其余异步加载:
<head>
<!-- 关键 CSS 内联(首屏渲染必需) -->
<style>
/* 首屏 header、hero 的样式 */
.header { ... }
.hero { ... }
</style>
<!-- 非关键 CSS 异步加载 -->
<link rel="preload" href="full.css" as="style" onload="this.rel='stylesheet'">
</head>
# 3.1 查找未使用的 CSS
# Chrome DevTools → Coverage 面板
F12 → Ctrl+Shift+P → "Coverage" → 录制 → 操作页面
# 红色 = 未使用,蓝色 = 已使用
# 或用工具
npx purgecss --css *.css --content *.html
# 4. 字体加载策略
# 4.1 FOIT 与 FOUT
FOIT (Flash of Invisible Text):字体加载前文字不可见
FOUT (Flash of Unstyled Text):字体加载前显示后备字体
推荐策略:先显示后备字体(FOUT),加载后平滑切换
@font-face {
font-family: 'CustomFont';
src: url('custom.woff2') format('woff2');
font-display: swap; /* 立即显示后备字体,加载后切换 */
}
/* font-display 取值:
auto → 浏览器默认(通常是 block)
block → FOIT,最多等 3 秒
swap → FOUT,立即显示后备字体(推荐)
fallback → 短 FOIT(~100ms),超时后后备
optional → 极短 FOIT,网络慢时放弃加载
*/
# 4.2 字体文件优化
/* 子集化:只加载需要的字符 */
/* 英文:https://github.com/Munter/subfont */
/* 中文:https://github.com/googlefonts/noto-cjk */
/* 预加载关键字体 */
<link rel="preload" href="custom.woff2" as="font" type="font/woff2" crossorigin>
# 5. CSS-in-JS 性能权衡
| 方案 | 运行时开销 | 样式提取 | 适用 |
|---|---|---|---|
| CSS Modules | 零(构建时) | ✅ 自动 | 中大型项目 |
| Tailwind | 零(构建时) | ✅ purge | 效率优先 |
| styled-components | 中(运行时注入) | ⚠️ 需额外配置 | 组件库 |
| Emotion | 中 | ⚠️ | 灵活场景 |
// ✅ 运行时零开销
import styles from './Button.module.css'; // CSS Modules
// ⚠️ 运行时计算样式
const Button = styled.button` // styled-components
background: ${props => props.primary ? 'blue' : 'gray'};
`;
# 6. 渲染性能诊断
# Chrome DevTools Performance 面板
F12 → Performance → 录制 → 操作页面
# 关注指标:
# ● Rendering 时间 > 50ms → CSS 样式计算瓶颈
# ● Painting 时间 > 10ms → 重绘开销大
# ● Forced reflow → 读写交替触发同步重排
# 视觉诊断:
F12 → Rendering 选项卡
✅ Paint Flashing → 绿色闪烁 = 重绘区域
✅ Layout Shift Regions → 蓝色 = CLS 偏移
✅ Layer Borders → 橙色边框 = 合成层
# 7. 速查表
| 问题 | 诊断 | 解法 |
|---|---|---|
| 页面滚动卡顿 | Performance 面板 Rendering 高 | 动画改用 transform/opacity |
| 首屏白屏 | 字体/图片阻塞 | font-display: swap, 关键 CSS 内联 |
| 大量未用 CSS | Coverage 面板红色占比高 | PurgeCSS 或按需加载 |
| 布局抖动(CLS) | Layout Shift Regions 蓝色闪烁 | 给图片/广告位预设宽高 |
| 交互反馈延迟 | 长任务阻塞主线程 | contain: layout 隔离 |
本系列完结。回到 CSS 篇目总览 或继续 Web 开发专栏首页。
上次更新: 2026/06/24, 10:13:05