# 合成层与GPU加速

深入理解浏览器合成层的创建条件、will-change 的正确用法、以及层爆炸的危害与避免

前置知识:第01章(浏览器渲染管线)


阅读指南(初学者必看)

为什么你需要学习合成层?

合成层是 H5 游戏性能优化的核心概念。不懂合成层,你可能会:

  • 滥用 will-change 导致层爆炸,低端手机直接闪退
  • 不知道 transform 为什么比 left/top 流畅
  • 无法解释为什么 100 个合成层占 800MB 显存

学完本章,你能回答:

  • 什么元素会成为合成层?
  • will-change 的正确用法是什么?
  • 如何检测和修复层爆炸?

一、什么是合成层?

生活类比:动画片的赛璐璐片

上世纪的动画片(如迪士尼早期作品)使用**赛璐璐片(Cel Animation)**技术:

  • 背景画在一张透明的塑料片上
  • 每个角色画在独立的塑料片上
  • 拍摄时把所有塑料片叠在一起
  • 角色移动时,只需要重画角色的那张片,背景完全不动

浏览器的合成层就是数字版的赛璐璐片。

为什么需要合成层?

如果没有合成层,整个页面就是一个巨大的画布。任何微小的变化(比如一个按钮变色)都需要重新绘制整个页面。

有合成层的情况

页面 = 背景层 + 游戏Canvas层 + UI层 + 弹窗层

当UI层的一个按钮变色时:
- 只重新绘制UI层
- 背景层、游戏Canvas层、弹窗层完全不动

没有合成层的情况

页面 = 一张巨大的画布

当UI层的一个按钮变色时:
- 重新绘制整个画布(包括背景、游戏、UI、弹窗)
- 工作量是有合成层的4倍!

二、合成层的创建条件

浏览器会根据以下规则自动决定哪些元素需要提升到独立的合成层:

自动创建合成层的情况

条件 CSS属性示例 原理
3D变换 transform: translate3d(0,0,0) GPU原生支持3D矩阵运算
2D变换 transform: translate/rotate/scale 同上
透明度动画 opacity: 0.5 且正在动画 GPU支持高效的Alpha混合
视频元素 video 视频解码在GPU上进行
Canvas/WebGL canvas 已经在GPU上渲染
iframe iframe 独立的文档,需要独立层
fixed定位 position: fixed 相对于视口,滚动时需要特殊处理
滚动容器 overflow: scroll 滚动内容需要独立层
will-change will-change: transform 显式告诉浏览器提前准备
覆盖在其他合成层上 z-index高于合成层 避免频繁的层合并

手动创建合成层的方法

/* 方法1:使用3D变换(最常用) */
.element {
  transform: translateZ(0);  /* 触发合成层,但不产生实际的3D效果 */
}

/* 方法2:使用will-change */
.element {
  will-change: transform;  /* 告诉浏览器这个元素将要被变换 */
}

/* 方法3:使用opacity(<1时) */
.element {
  opacity: 0.99;  /* 略小于1也会创建合成层 */
}

注意:这些方法只是提示浏览器创建合成层,浏览器有权决定是否真的创建。


三、will-change 详解

什么是 will-change?

will-change 是 CSS 属性,用来告诉浏览器哪些属性将要发生变化,让浏览器提前做优化准备。

生活类比:就像你要搬家,提前一周告诉物业。物业可以提前准备好电梯、停车位,搬家当天就会顺利很多。如果不提前说,搬家当天可能电梯被占、车位没有,效率很低。

语法

/* 提示浏览器:这个元素的transform将要变化 */
.element {
  will-change: transform;
}

/* 提示浏览器:transform和opacity都要变化 */
.element {
  will-change: transform, opacity;
}

/* 取消提示(动画结束后应该取消) */
.element {
  will-change: auto;
}

will-change 的正确用法

错误用法1:给所有元素加 will-change

/* 千万不要这样做! */
* {
  will-change: transform;
}

这会导致层爆炸

错误用法2:页面加载时就加 will-change

/* 不推荐:页面一加载就提示 */
.animated-element {
  will-change: transform;  /* 从页面加载开始就占用内存 */
}

这会导致合成层一直存在,浪费内存。

正确用法:动态添加和移除

// 动画开始前添加 will-change
element.addEventListener('mouseenter', () => {
  element.style.willChange = 'transform';
});

// 动画结束后移除 will-change
element.addEventListener('transitionend', () => {
  element.style.willChange = 'auto';
});

或者使用CSS类切换

.animated-element {
  /* 默认不加 will-change */
}

.animated-element.is-animating {
  will-change: transform;
}
// 动画开始时添加类
element.classList.add('is-animating');

// 动画结束后移除类
element.addEventListener('transitionend', () => {
  element.classList.remove('is-animating');
});

will-change 的性能影响

正面影响

  • 浏览器提前创建合成层,动画开始时没有延迟
  • 动画期间不需要重新布局、绘制、光栅化,只合成

负面影响

  • 每个合成层都需要额外的内存(大约几MB,取决于层的大小)
  • 层太多时,合成阶段本身的开销也会增加
  • 层的创建和销毁也有开销

最佳实践

  • 只在需要动画的元素上使用
  • 动画开始前添加,动画结束后移除
  • 不要同时给太多元素使用(建议不超过10个)

四、层爆炸(Layer Explosion)

什么是层爆炸?

当你给太多元素创建了合成层时,就会出现层爆炸。就像你把房间里每个小物件都装在独立的盒子里,盒子太多,房间反而更乱了。

层爆炸的危害

1. 内存暴涨

每个合成层都需要一块内存来存储位图(Bitmap)。

假设屏幕分辨率:1920 x 1080
每个像素4字节(RGBA)
一个全屏层 = 1920 x 1080 x 4 = 8,294,400 字节 ≈ 8MB

如果有50个层(层爆炸):
总内存 = 50 x 8MB = 400MB

在手机上,这可能导致应用被系统杀掉(OOM)!

2. 合成开销增加

合成阶段需要把所有层按顺序叠加。层越多,合成的计算量越大:

5个层:合成时间 ≈ 1ms
50个层:合成时间 ≈ 10ms
500个层:合成时间 ≈ 100ms(严重掉帧!)

3. 首次渲染延迟

页面首次显示时,所有合成层都需要被光栅化。层越多,首次渲染时间越长。

层爆炸的实际案例

案例1:滥用 translateZ(0)

/* 某个开发者为了加速,给所有元素加了translateZ(0) */
* {
  transform: translateZ(0);
}

结果:一个简单页面有100+个DOM元素,创建了100+个合成层,内存占用超过500MB,在低端手机上直接闪退。

案例2:复杂的嵌套动画

<div class="page">          <!-- 合成层1 -->
  <div class="header">      <!-- 合成层2 -->
    <div class="logo">      <!-- 合成层3 -->
      <img src="logo.png">  <!-- 合成层4 -->
    </div>
  </div>
  <div class="content">     <!-- 合成层5 -->
    <div class="sidebar">   <!-- 合成层6 -->
      <div class="menu">    <!-- 合成层7 -->
        <div class="item">  <!-- 合成层8 -->
          ...
        </div>
      </div>
    </div>
  </div>
</div>

如果每一层都因为某种原因(如CSS动画)被提升为合成层,一个普通页面可能有20-30个合成层,内存占用巨大。

如何避免层爆炸?

原则1:不要轻易给所有元素加合成层

/* 错误 */
* { transform: translateZ(0); }

/* 正确:只给需要动画的元素加 */
.animated-element {
  transform: translateZ(0);
}

原则2:用Chrome DevTools检查层数

  1. 打开Chrome DevTools
  2. 按 Esc -> More tools -> Layers
  3. 查看当前页面的层数
  4. 如果层数超过20个,需要检查是否有不必要的层

原则3:合并相邻的层

如果多个相邻元素都有动画,考虑把它们放在同一个父容器中,只给父容器加合成层:

<!-- 优化前:每个按钮都是独立层 -->
<button class="btn1">Button 1</button>  <!-- 层1 -->
<button class="btn2">Button 2</button>  <!-- 层2 -->
<button class="btn3">Button 3</button>  <!-- 层3 -->

<!-- 优化后:只有一个父层 -->
<div class="button-group">  <!-- 层1 -->
  <button>Button 1</button>
  <button>Button 2</button>
  <button>Button 3</button>
</div>

原则4:动画结束后移除will-change

element.addEventListener('transitionend', () => {
  element.style.willChange = 'auto';
});

五、合成层与H5游戏的关系

H5游戏中的典型层结构

H5游戏页面的典型层结构:

Layer 0: 页面背景层(纯色或背景图)
  |
  +-- Layer 1: 游戏主Canvas层(Cocos/Laya/Phaser渲染)
  |     |
  |     +-- 这一层由游戏引擎管理,包含所有游戏元素
  |     +-- 引擎内部的合批优化已经处理好了
  |
  +-- Layer 2: 游戏UI层(HTML/CSS实现的UI)
  |     |
  |     +-- 分数显示
  |     +-- 血量条
  |     +-- 技能按钮
  |
  +-- Layer 3: 弹窗/遮罩层
  |     |
  |     +-- 设置弹窗
  |     +-- 暂停菜单
  |
  +-- Layer 4: 广告/分享层(最高层)

优化建议

1. 游戏Canvas应该独占一个合成层

#game-canvas {
  transform: translateZ(0);  /* 确保Canvas在独立层上 */
}

这样游戏引擎的渲染不会和页面其他元素的渲染互相干扰。

2. UI动画使用transform

/* 正确:使用transform做UI动画 */
.ui-panel {
  transform: translateY(100%);
  transition: transform 0.3s;
}
.ui-panel.show {
  transform: translateY(0);
}

/* 错误:使用top做动画(会触发重排) */
.ui-panel {
  top: 100%;
  transition: top 0.3s;
}
.ui-panel.show {
  top: 0;
}

3. 避免在游戏循环中修改DOM样式

// 错误:在requestAnimationFrame中修改DOM样式
function gameLoop() {
  // 这会在每帧都触发重排!
  document.getElementById('score').style.width = score + 'px';
  requestAnimationFrame(gameLoop);
}

// 正确:用Canvas或transform更新UI
function gameLoop() {
  // 使用transform,只触发合成
  document.getElementById('score').style.transform = 'scaleX(' + (score / maxScore) + ')';
  requestAnimationFrame(gameLoop);
}

自问自答

Q:如何查看一个页面有多少个合成层? A:打开Chrome DevTools,按Esc打开Console,点击菜单(三个点),选择More tools -> Layers。在Layers面板中可以看到所有合成层,包括每个层的大小、内存占用和创建原因。

Q:合成层和z-index有什么关系? A:z-index决定层的堆叠顺序,但不直接决定是否创建合成层。一个元素可以有很高的z-index但没有合成层,也可以有合成层但z-index很低。但是,如果一个元素覆盖了合成层,浏览器可能会自动把它也提升为合成层(为了避免频繁的层合并)。

Q:为什么有时候加了transform但没有创建合成层? A:浏览器有权根据内存和性能情况决定是否创建合成层。如果当前内存紧张,或者元素太小不值得创建层,浏览器可能会忽略提示。另外,某些transform值(如transform: none)不会创建合成层。

Q:合成层和硬件加速是一回事吗? A:不完全是一回事,但密切相关。硬件加速是指使用GPU进行渲染计算。合成层是硬件加速的一种实现方式:把元素提升到独立的层,让GPU来合成。但不是所有的硬件加速都需要合成层(如WebGL直接在GPU上渲染,不需要合成层)。


实践任务

  • 任务1:用 Chrome DevTools Layers 面板分析一个复杂网页的合成层数量和每层大小
  • 任务2:给一个元素添加 will-change: transform,观察 Layers 面板的变化
  • 任务3:制造层爆炸(给20个div加 translateZ(0)),观察显存占用变化
  • 任务4:修复层爆炸,用 z-indexposition 隔离层叠上下文
  • 任务5:对比有/无合成层的动画帧率(用 Performance 面板录制)

与其他章节的关联

本章内容 关联章节 关联点
合成层 第01章 渲染管线的第5~8阶段
层爆炸 第05章 合成层占用的显存是游戏内存的大头
will-change 第03章 动画优化与重排重绘的关系
GPU合成 2_2_h5-rendering-mastery GPU 渲染管线的最后一步

上一章:01-浏览器渲染管线详解 下一章:03-重排与重绘优化