跳到主要内容

CSS 在性能优化方面有哪些实践?

面试速答(30 秒版 TL;DR)

  • CSS 性能优化分两类:
    • 交付性能(下载/解析/阻塞渲染):少发、快发、别阻塞首屏(关键 CSS、移除无用 CSS、避免 @import、压缩缓存)。
    • 运行时性能(每次渲染的计算量):少触发、少重算(避免触发布局的动画、降低重排重绘、用 contain/content-visibility 限定影响范围)。
  • 最常用的抓手
    • 交付:Purge/Tree-shaking 未使用 CSS、拆包(路由级/组件级)、关键 CSS(首屏内联或优先加载)。
    • 运行时:动画只动 transform/opacity,减少大面积重绘(阴影/滤镜/渐变慎用),长列表用 content-visibility: auto
  • 验证:Chrome DevTools 的 PerformanceRecalculate Style / Layout / Paint / Composite 时间占比;Coverage 看未使用 CSS;用 Paint flashing 观察重绘区域。

心智模型:CSS 影响渲染的 4 个阶段

把浏览器渲染过程简化成四步(面试只要能说清“优化点在哪”):

  1. Style(样式计算):选择器匹配 + 计算最终样式。
  2. Layout(布局/回流):算几何信息(位置/大小)。
  3. Paint(绘制):把像素画到位图(背景、文字、阴影等)。
  4. Composite(合成):把多个图层拼起来(通常 GPU 参与)。

经验法则(非常实用):

  • 能只走 Composite,就别走 Layout/Paint:所以动画优先 transform/opacity
  • 影响范围越小越好:用 contain、合理的组件边界、避免全局样式引发大面积重算。

实践 1:交付性能(让 CSS 更小、更早、更不阻塞)

1)移除无用 CSS(体积和解析时间都下降)

适用场景:组件库、历史包袱、全站大样式表。

  • 用 DevTools Coverage 找未使用 CSS,配合构建工具做 Purge(例如按路由/组件拆分、按类名扫描)。
  • 风险点:动态拼接类名(如 className={'btn-' + type})会被误删,需要白名单或改写为显式枚举。

2)避免 @import(额外阻塞和串行请求)

@import 常见问题是:浏览器要先下载并解析当前 CSS,才会继续请求被 import 的 CSS,容易拖慢关键路径。

更推荐用构建期合并,或在 HTML 里用多个 <link rel="stylesheet">(配合 HTTP/2/3 与缓存策略)。

3)关键 CSS(Critical CSS)优先

核心目标:首屏别等一大坨 CSS 才开始渲染

  • 把首屏必须的样式做成关键 CSS:内联到 HTML 或高优先级加载。
  • 非首屏样式延后:按路由/页面拆分,或用 media 条件加载(例如打印样式、低优先级模块)。
取舍

关键 CSS 不是越多越好:内联太多会让 HTML 变大、阻塞 TTFB 后的下载,也会导致缓存粒度变差。

4)压缩、缓存、减少重复下载

  • 生产环境:CSS minify,开启 gzip/brotli
  • 用内容哈希做长期缓存(app.[hash].css),确保“变更才更新”。

实践 2:运行时性能(少重排、少重绘、少大范围重算)

1)动画与过渡:避免触发布局(Layout)

优先动画 transform/opacity,避免 top/left/width/height/margin 这类会触发布局的属性。

对比示例:

/* 不推荐:通常会触发布局(Layout)*/
@keyframes move-bad {
from {
left: 0;
}
to {
left: 300px;
}
}

/* 更推荐:通常可以走合成(Composite)*/
@keyframes move-good {
from {
transform: translateX(0);
}
to {
transform: translateX(300px);
}
}

will-change 只在“即将发生动画”时短期使用:

.card.is-animating {
will-change: transform;
}
面试加分点

will-change 是提示优化,不是强制优化;滥用会增加内存占用和图层管理开销,反而变慢。

2)减少重绘(Paint):慎用高成本视觉效果

这些常见效果容易放大 Paint 成本(尤其大面积、动画时):

  • 大模糊 box-shadowfilter: blur()backdrop-filter
  • 大面积半透明叠加、复杂渐变、频繁变化的背景

优化思路:

  • 视觉上能“静态化”就别动画化(例如阴影不动、只动位移)。
  • 能缩小重绘区域就缩小(组件隔离、减少覆盖层范围)。

3)长列表/大 DOM:用 content-visibility 跳过屏外渲染

适用:长文档、瀑布流、评论列表、复杂卡片列表。

.listItem {
content-visibility: auto;
contain-intrinsic-size: 1px 240px;
}

要点:

  • 屏外元素可以跳过 style/layout/paint,滚动到可视区再渲染。
  • 注意兼容性与占位:常配合 contain-intrinsic-size 给一个合理的“预估尺寸”,避免滚动跳动。

4)用 contain 限定影响范围(降低“牵一发而动全身”)

组件边界清晰时可以考虑:

.widget {
contain: layout paint style;
}

好处:把样式计算、布局、绘制的影响限制在组件内,减少全局连锁反应。

风险:contain 会改变某些布局与定位行为(例如尺寸依赖、滚动容器、定位参考等),需要在真实页面回归验证。


实践 3:可观测与验收(不要“凭感觉优化”)

建议按“先定位瓶颈,再优化,再验证”的流程走:

  1. DevTools Performance
    • 重点看 Recalculate StyleLayoutPaint 是否异常占比。
  2. DevTools Coverage
    • 看 CSS 未使用比例,决定是否需要 Purge/拆分。
  3. Rendering 工具:
    • 开启 Paint flashing,观察是否出现“大面积闪绿”的重绘。

验收时可以把 CSS 优化和指标绑定:

  • LCP:常被“渲染阻塞 CSS”“大体积 CSS”拖慢。
  • CLS:常被“晚到的字体样式/字号变化”“未预留空间的组件样式变化”拉高。
  • INP:频繁的样式重算/布局抖动会增加主线程负担,影响交互响应。

典型题 & 标准答法

Q1:为什么 CSS 会阻塞渲染?怎么优化?

答法要点:

  • 浏览器需要先拿到 CSS 并构建 CSSOM,才能进行样式计算与首次渲染;所以关键 CSS 是渲染阻塞资源
  • 优化思路:减少关键 CSS 体积、拆分非关键 CSS、避免 @import、关键 CSS 优先(内联/优先加载)、长期缓存。

Q2:重排(reflow/layout)和重绘(repaint/paint)有什么区别?怎么减少?

答法要点:

  • Layout 是几何计算;Paint 是像素绘制;Composite 是图层合成。
  • 减少方法:避免频繁触发布局(尤其动画),组件隔离(contain),减少大面积绘制,动画优先 transform/opacity

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

答法要点:

  • 用途:告诉浏览器某个属性即将变化,可能提前做图层/缓存准备,减少抖动。
  • 不能滥用:会占用更多内存/图层资源,管理成本上升,可能导致整体更慢。

易错点/坑

  • Purge 过度:误删动态类名导致线上样式丢失;需要白名单或避免运行时拼接类名。
  • 过度拆分 CSS:拆太细会增加请求/管理复杂度,甚至引发 FOUC(样式闪烁)。
  • 滥用 will-change:把一堆元素都提升为图层,内存爆、合成开销上升。
  • 给大区域做动画特效:大模糊阴影/滤镜/渐变动画会让 Paint 成本飙升。

速记要点(可背诵)

  • CSS 优化两条线:交付(更小更早) + 运行时(少 Layout/Paint,多 Composite)
  • 动画:只动 transform/opacitywill-change 少用且短用。
  • 大列表:content-visibility: auto;组件隔离:contain