技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTCSS

谈谈 CSS 硬件加速(GPU 加速)

所属馆藏
CSS
文件格式
Markdown
原始路径
CSS/29-谈谈CSS3 硬件加速(GPU 加速)
本文目录14 个章节
  1. 一、为什么页面会卡顿?
  2. 二、浏览器中的几个渲染阶段
  3. 三、什么是“硬件加速”?
  4. 四、不要把 translateZ(0) 当作通用加速开关
  5. 五、哪些场景需要重点关注?
  6. 六、will-change 的正确使用方式
  7. 七、图层、纹理与内存
  8. 八、Alpha 混合的基本原理
  9. 九、浏览器渲染性能优化方法
  10. 十、不同动画方案的取舍
  11. 十一、如何验证优化是否有效?
  12. 十二、常见错误认识
  13. 总结
  14. 参考资料

谈谈 CSS 硬件加速(GPU 加速)

Category(分类): CSS Status: 未知

“CSS3 硬件加速”是前端开发中常用的口语说法。更准确的描述是:浏览器可能把部分元素提升为独立合成层,并在合成阶段使用 GPU 或其他硬件加速路径。CSS 属性本身不能保证一定使用 GPU,也不能保证一定带来性能提升。

一、为什么页面会卡顿?

页面动画卡顿通常不是因为“没有打开 GPU”这么简单,而是因为某一帧需要完成的工作超过了预算。以 60Hz 屏幕为例,浏览器大约每 16.7ms 需要准备一帧;如果布局、绘制、图片解码、脚本执行或合成耗时过长,就可能出现掉帧。

常见原因包括:

  • JavaScript 长任务阻塞主线程;
  • 频繁改变会影响布局的属性;
  • 大面积重绘或复杂阴影、滤镜;
  • 大尺寸图片解码、缩放或上传为纹理;
  • 过多合成层和过大的图层纹理;
  • 频繁读取布局信息并交替写入样式;
  • 页面滚动时需要绘制大量内容。

因此,优化目标不是简单地“开启 GPU”,而是减少每帧必须完成的布局、绘制和合成工作,并通过性能工具验证效果。

二、浏览器中的几个渲染阶段

现代浏览器的具体实现会因浏览器、平台和内容而不同,但可以用下面的流程理解网页渲染:

HTML / CSS / JavaScript
        ↓
DOM / CSSOM
        ↓
样式计算(Style)
        ↓
布局(Layout)
        ↓
绘制(Paint)
        ↓
栅格化(Rasterize)
        ↓
合成(Composite)
        ↓
屏幕输出

1. 网络加载与解析

浏览器会请求 HTML、CSS、JavaScript、图片和字体等资源,同时进行预加载扫描、HTML 解析和资源调度。

HTML 解析会生成 DOM,CSS 解析会生成 CSSOM 或等价的样式数据。外部 CSS 通常会阻塞首次渲染,因为浏览器需要知道样式后才能正确计算页面,但这不等于 HTML 解析在所有情况下都会完全暂停。网络请求、预加载和部分解析工作可能并行进行。

样式规则与 CSSOM 示意图

2. 样式计算和布局

浏览器将 DOM 与 CSS 规则结合,为元素计算最终样式,并确定元素的尺寸和位置。这个阶段通常称为布局(Layout),历史资料中也常称为回流(Reflow)。

如果改变了会影响尺寸或位置的属性,例如 widthheightmarginpaddingtopleft 等,浏览器可能需要重新进行布局。

布局计算示意图

3. 绘制

布局完成后,浏览器会把背景、边框、文字、图片、阴影等内容组织成绘制指令或显示列表。绘制结果可能被缓存,也可能在后续变化时重新绘制。

4. 栅格化和合成

绘制指令需要被栅格化为像素数据,之后浏览器根据图层顺序、变换、透明度和裁剪区域进行合成。部分内容可以由 GPU 参与栅格化或合成,但具体路径由浏览器决定。

图形处理流程示意图

三、什么是“硬件加速”?

在前端语境中,硬件加速通常指浏览器利用 GPU 或其他专用硬件完成栅格化、纹理处理和图层合成的一部分工作。它并不是 CSS 规范中的一个开关,也没有一个通用的 enable-gpu 属性。

以下属性在某些场景下可能促使浏览器采用更适合合成的处理方式,但都不能保证创建独立图层:

transform
opacity
filter
will-change

它们的效果大致如下:

属性可能的收益注意事项
transform动画元素位置、缩放或旋转时通常不直接触发布局仍可能需要绘制或重新栅格化
opacity透明度动画在独立层上通常适合合成元素仍可能响应事件和占据布局空间
filter可以在合成或绘制阶段处理视觉效果模糊、阴影等大面积滤镜可能非常昂贵
will-change提前提示浏览器属性即将变化可能增加内存和图层开销,不应长期滥用

transformopacity 并不保证“不会 repaint”

transformopacity 通常不会直接改变普通流中的尺寸和位置,因此在合适场景下可以避免布局。但它们仍可能触发:

  • 元素所在图层的重新绘制;
  • 纹理重新栅格化;
  • 大面积合成;
  • 滤镜或阴影的重新计算。

所以不能绝对地说:

使用 transform 后永远不会发生 repaint,页面一定更快。

是否优化成功,需要使用浏览器性能面板观察 Layout、Paint、Raster 和 Composite 的耗时。

四、不要把 translateZ(0) 当作通用加速开关

早期浏览器和移动端开发中,经常看到以下写法:

.hack {
  transform: translateZ(0);
}

或者:

.hack {
  transform: translate3d(0, 0, 0);
}

这类写法曾经被用来尝试触发独立合成层,但它不是标准的 GPU 开关。现在无条件使用可能带来:

  • 更多合成层;
  • 更高的 GPU 内存使用;
  • 图层上传和合成开销;
  • 文本或细线在缩放、非整数像素位置下变模糊;
  • 移动设备功耗上升。

现代项目应先写出语义清晰的动画,再通过性能工具判断是否需要额外优化,而不是先给每个元素添加 translateZ(0)

原文中提到的 transition3d 并不是 CSS 函数,正确名称是 translate3d()rotateZ(360deg) 也不能保证开启硬件加速,且可能改变元素的渲染方式。

backface-visibilityperspective

下面的属性主要用于 3D 变换场景:

.card {
  perspective: 1000px;
}

.card__face {
  backface-visibility: hidden;
}

它们不是通用的闪烁修复或 GPU 加速方案。遇到闪烁、抖动或白屏时,应同时检查图层数量、图片纹理、变换缩放、裁剪和浏览器实现。

五、哪些场景需要重点关注?

以下场景可能需要性能分析:

  1. 大尺寸图片参与连续动画;
  2. 大背景图配合 background-size: cover 并且页面滚动;
  3. 大量 DOM 元素同时进行 transitiontransform@keyframes 动画;
  4. 大量半透明图层、阴影或滤镜叠加;
  5. 使用 Sprite 或序列帧图片进行动画;
  6. 移动端低内存设备上的长列表和复杂滚动区域。

这些只是需要检查的场景,并不意味着加上某个 CSS 属性就一定能解决问题。大尺寸图片还应进行压缩、分辨率控制、懒加载和纹理尺寸评估。

矢量图形与栅格图像示意图

六、will-change 的正确使用方式

will-change 用于提前告诉浏览器某个元素的某些属性即将发生变化:

will-change: auto;
will-change: scroll-position;
will-change: contents;
will-change: transform;
will-change: opacity;
will-change: left, top;

它是性能提示,不是“开启 GPU”的命令。浏览器可能根据提示提前准备资源,也可能因为成本、内存和实现策略而不创建独立图层。

不要长期给所有元素设置 will-change

不推荐:

* {
  will-change: transform;
}

这样可能导致大量元素占用额外内存,反而降低性能。应只在确实即将变化的元素上短时间使用。

使用状态类

.card {
  transition: transform 0.3s ease;
}

.card.is-moving {
  will-change: transform;
}

.card:hover {
  transform: scale(1.03);
}

如果要使用 will-change,可以在交互开始前添加状态类,并在动画结束后移除:

const trigger = document.querySelector('[data-trigger]')
const target = document.querySelector('[data-target]')

if (trigger && target) {
  trigger.addEventListener('pointerdown', () => {
    target.style.willChange = 'transform'
  })

  target.addEventListener('transitionend', (event) => {
    if (event.propertyName === 'transform') {
      target.style.willChange = 'auto'
    }
  })

  target.addEventListener('animationend', () => {
    target.style.willChange = 'auto'
  })
}

如果动画被取消、元素被移除或交互可以重复触发,还需要在 transitioncancelanimationcancel 或业务清理逻辑中恢复状态。

Hover 方式的局限

可以使用父元素的 hover 提示:

.will-change-parent:hover .will-change {
  will-change: transform;
}

.will-change {
  transition: transform 0.3s;
}

.will-change:hover {
  transform: scale(1.5);
}

但 hover 触发时机、触摸设备行为和浏览器是否来得及准备资源都可能不同,因此它不是性能保证。对复杂动画,应该用脚本或动画生命周期更明确地管理。

七、图层、纹理与内存

GPU 处理图片时,图片可能会作为纹理上传并参与合成。大图片和大量图层会增加:

  • GPU 内存占用;
  • 图片上传带宽;
  • 栅格化时间;
  • 合成时间;
  • 移动设备功耗。

因此,GPU 参与并不意味着“资源越多越快”。应控制图片的实际尺寸、压缩质量和显示分辨率,避免让每个元素都成为独立图层。

使用变换或合成层后,文字在某些浏览器、系统和非整数像素位置下可能出现抗锯齿变化或模糊。这不是所有设备上的必然现象,遇到问题应检查:

  • 是否使用了缩放;
  • 是否落在半像素位置;
  • 是否创建了 3D 合成层;
  • 设备像素比和浏览器实现;
  • 字体本身是否已经加载完成。

八、Alpha 混合的基本原理

页面中的透明效果通常需要将前景颜色和背景颜色进行混合。使用未预乘 alpha 的颜色表示时,源覆盖(source-over)的基本公式可以写成:

Cout = Cs × As + Cd × (1 - As)
Aout = As + Ad × (1 - As)

其中:

  • Cs:源颜色;
  • As:源 alpha;
  • Cd:目标颜色;
  • Ad:目标 alpha;
  • Cout:混合后的颜色;
  • Aout:混合后的 alpha。

如果使用预乘 alpha 的颜色分量,颜色公式可以写成:

Cout = Cs + Cd × (1 - As)

实际浏览器会根据颜色空间、预乘方式和渲染管线进行处理。不要因为 alpha 混合存在就简单删除透明效果,应该先通过性能分析确认透明层是否造成了大面积重绘或过度合成。

光栅化和屏幕像素示意图

九、浏览器渲染性能优化方法

减少一帧中需要完成的工作,通常比强制开启某种硬件加速更重要。

1. 减少不必要的 DOM 更新

虚拟 DOM、批量更新和组件框架可以帮助组织更新,但并不保证一定比原生 DOM 更快。无论使用哪种方案,最终仍然需要更新真实 DOM,并执行浏览器的样式计算、布局、绘制和合成。

建议:

  • 合并同一帧内的状态更新;
  • 使用事件委托减少监听器数量;
  • 对长列表使用虚拟列表;
  • 只更新真正发生变化的节点;
  • 避免在滚动事件中执行长任务。

2. 优先切换 class,而不是反复拼接 cssText

不推荐反复写入多个行内属性:

const element = document.querySelector('#test')

element.style.margin = '5px'
element.style.width = '100px'
element.style.borderRight = '2px solid currentColor'

更推荐使用 class:

.is-active {
  width: 100px;
  margin: 5px;
  border-right: 2px solid currentColor;
}
element.classList.add('is-active')

如果确实需要一次性修改行内样式,应注意 cssText 可能覆盖已有声明:

element.style.cssText = `
  margin: 5px;
  width: 100px;
  border-right: 2px solid currentColor;
`

3. 避免布局抖动(Layout Thrashing)

读取布局属性可能触发同步布局,尤其是在前面刚写入样式的情况下。常见布局读取属性包括:

  • offsetWidthoffsetHeight
  • offsetTopoffsetLeft
  • clientWidthclientHeight
  • scrollWidthscrollHeight
  • getBoundingClientRect()
  • 某些情况下的 getComputedStyle()

原文示例中的 elment 是拼写错误,应为 element。不推荐在循环中反复读写布局:

for (let i = 0; i < elements.length; i += 1) {
  elements[i].style.width = `${box.offsetWidth}px`
}

可以先读取一次,再批量写入:

const width = box.offsetWidth

for (const element of elements) {
  element.style.width = `${width}px`
}

更复杂的动画可以使用 requestAnimationFrame,将读取和写入安排在合适的帧中。

4. 正确理解 offsetWidth

offsetWidth 返回元素的布局宽度,单位是 CSS 像素,通常包括:

  • 内容宽度;
  • 左右内边距;
  • 左右边框;
  • 垂直滚动条(如果存在)。

它不包括外边距,并且会取整为整数。不能简单写成:

offsetWidth = width + padding + border

因为结果还会受到 box-sizing、滚动条和元素类型影响。默认 content-box 下可以近似这样理解;border-box 下,声明的 width 通常已经包含内边距和边框。

盒模型与 offsetWidth 示意图

5. 使用 transformopacity 做合适的动画

对于只改变视觉位置的动画,通常可以考虑使用 transform

.card {
  transition: transform 0.3s ease, opacity 0.3s ease;
}

.card.is-moving {
  transform: translate3d(0, -8px, 0);
  opacity: 0.8;
}

与直接动画 topleft 相比,transform 通常不需要重新计算普通流布局。但这并不代表它永远更快,仍需要确认:

  • 元素是否覆盖了很大的区域;
  • 是否包含复杂阴影和滤镜;
  • 是否产生了过多合成层;
  • 是否因为缩放导致重新栅格化。

opacityvisibility 也不能简单互换:

  • opacity: 0 仍可能响应鼠标事件并占据布局空间;
  • visibility: hidden 通常不可见且不接受交互,但仍占据空间;
  • display: none 会移出布局,但切换时可能引发较大范围布局更新。

如果透明元素不应交互,可以额外设置:

.is-hidden {
  opacity: 0;
  pointer-events: none;
}

使用 transform 和 opacity 的动画流程示意图

十、不同动画方案的取舍

1. CSS transform 动画

适合简单的位移、缩放、旋转和透明度变化:

.ball {
  animation: move 1s ease-in-out infinite alternate;
}

@keyframes move {
  from {
    transform: translateX(0);
  }

  to {
    transform: translateX(200px);
  }
}

2. Sprite 或序列帧动画

Sprite 和序列帧动画可以表现复杂画面,但资源体积、图片尺寸和纹理内存都可能较大。帧数越多不一定越流畅,还要考虑图片解码和上传成本。

序列帧或 Sprite 动画示意图

3. Canvas 和 WebGL

Canvas、WebGL 或其他渲染引擎适合大量图形、粒子和游戏场景,但它们并不是所有网页动画的默认最佳方案。使用前应考虑:

  • 是否需要复杂图形绘制;
  • 是否需要文本选择和无障碍语义;
  • 是否需要响应式布局;
  • 是否需要处理高 DPR 设备;
  • 是否有足够的开发和维护成本。

原文提到的 Egret 属于较早时期的游戏和互动内容工具,可作为历史资料了解,但不应作为现代网页动画的通用推荐。

4. 动画库

Animate.css 等 CSS 动画库可以快速提供预设效果,但性能仍取决于具体动画属性、元素面积和使用数量。使用动画库不代表自动获得最佳性能:

十一、如何验证优化是否有效?

不要只根据“使用了 GPU 属性”判断性能。可以使用浏览器开发者工具:

  1. Performance 面板录制动画和滚动过程;
  2. 查看长任务、Layout、Paint、Raster 和 Composite 的耗时;
  3. 使用 Rendering 面板中的 Paint flashing 观察重绘区域;
  4. 使用 Layers 面板检查合成层数量和纹理尺寸;
  5. 观察内存和设备温度,尤其是移动端;
  6. 对比优化前后的帧率和掉帧情况。

浏览器渲染流程与性能分析示意图

合成阶段示意图

如果动画元素很小、页面本身不复杂,强行创建合成层可能没有收益。优化应建立在真实测量之上。

十二、常见错误认识

错误一:给所有元素加 translateZ(0)

这可能创建大量合成层,增加内存和合成成本。应只在确认问题并测试有效后使用。

错误二:will-change 设置得越多越好

will-change 是提示,不是加速命令。长期设置会消耗资源,甚至让页面更慢。

错误三:transformopacity 永远不会触发重绘

它们通常可以避免布局,但仍可能触发绘制、重新栅格化或合成。

错误四:GPU 加速一定比 CPU 绘制快

不同设备、不同图层大小和不同视觉效果的成本不同。小元素或简单页面上,额外合成层可能反而增加开销。

错误五:只要帧率高,内存就没有问题

过大的纹理和过多合成层可能造成内存压力、页面闪烁、纹理被回收或移动端崩溃。性能需要同时观察 CPU、GPU、内存和功耗。

总结

  1. “CSS3 硬件加速”更准确地说是浏览器可能使用硬件栅格化或合成;
  2. CSS 属性不能保证开启 GPU,也不能保证一定提升性能;
  3. transformopacity 通常适合部分动画,但仍可能触发绘制或重新栅格化;
  4. filter、大图片、透明层和阴影可能带来较高绘制成本;
  5. translateZ(0)translate3d() 等不是现代项目的通用 GPU Hack;
  6. will-change 应短时间、少量、按需使用,并在动画结束后清理;
  7. offsetWidth 等布局读取应避免与样式写入交替进行;
  8. offsetWidth 是 CSS 像素下的布局宽度,不是物理像素宽度;
  9. 虚拟 DOM、Canvas 和动画库都不是自动的性能保证;
  10. 最可靠的优化方式是使用 Performance、Layers 和 Paint flashing 等工具测量,再针对实际瓶颈处理。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS