1. 核心底层八股:浏览器的渲染管线 (Render Pipeline)
这是所有性能优化的基石,面试官如果深挖,一定会问:为什么 transform 做动画就不卡?
- 标准渲染流程:JavaScript -> Style(计算样式) -> Layout(重排/回流) -> Paint(重绘) -> Composite(合成)。
- 卡顿的元凶:如果你用
width、top、margin来做翻书动画,每一帧都会触发 Layout 和 Paint,这些都在 CPU 的主线程运行,非常昂贵,必然掉帧。 - 硬件加速的原理:当你使用
transform或opacity做动画时,浏览器极其聪明,它会直接跳过 Layout 和 Paint,直接进入 Composite 阶段。并且,它会将这个元素提取到一个独立的图层 (Compositing Layer),交由 GPU 去专门处理。GPU 天生就是做矩阵变换和图层叠加的,所以动画会如丝般顺滑。
2. 实战考点:怎么调用/触发 GPU 硬件加速?
面试官会问:除了写 transform,还有什么方法强制开启硬件加速?
- 经典 Hack 写法 (Translate3D):
transform: translateZ(0)或transform: translate3d(0, 0, 0)。虽然位移是 0,但这是在欺骗浏览器:“嘿,这是一个 3D 元素,赶紧把它单独放到 GPU 图层里去!” - 现代优雅写法 (Will-Change):
will-change: transform;。这是明确告诉浏览器:“这个元素的 transform 属性马上要变了,请提前做好 GPU 资源分配和图层提升准备。” - 还有哪些情况会自动触发:
<video>标签、<canvas>、CSS Filter 等。
3. 样式考点:实现立体书必须要懂的 3D 属性
仅仅靠位移是不够的,实现立体翻书效果,你的 CSS 里必须要出现这三个核心属性:
- perspective: 800px; (透视/景深):
- 没有透视,翻书看起来就像是一个面在不断变窄;加了透视,才有“近大远小”的 3D 纵深感。数值越小,透视感越畸形(贴脸看);数值越大,越平缓。
- transform-style: preserve-3d; (保留 3D 空间):
- 必须加在书本的父容器上!如果不加,当你翻开封面时,封面会被拍扁在父容器的 2D 平面上。加了它,子元素才能真正脱离平面,在 Z 轴上穿插。
- backface-visibility: hidden; (背面不可见):
- 超级重要的优化点!当书页翻过去 180 度时,背面其实是不应该被看到的。加上这个属性,GPU 就不需要去计算和绘制翻过去之后的背面像素,极大节省了渲染开销。
4. 架构考点:JS 怎么配合优化 3D 动画?
虽然渲染交给了 GPU,但“翻书的角度(进度)”往往是由用户手指滑动触发的,这就需要 JS 出马了。面试官会问:滑动翻书时,JS 怎么写最流畅?
- 使用 requestAnimationFrame (rAF):
- 绝不要用
setTimeout或setInterval去更新rotateY的角度。rAF 会让你的 JS 执行频率与浏览器的屏幕刷新率(通常是 60fps)绝对同步,避免掉帧。
- 绝不要用
- 事件节流 (Throttling) 与被动监听 (Passive Events):
- 用户的
touchmove事件触发频率极高。在绑定监听器时加上,告诉浏览器“我不会调用preventDefault()阻止默认滚动”,这样浏览器主线程就不会等待你的 JS 执行,滑动会立刻响应。
- 用户的
- 分离读写操作 (避免 Layout Thrashing):
- 如果你在滑动时,先读取一下
element.clientWidth,紧接着立刻修改element.style.transform,这会强制浏览器提前进行重排(强制同步布局)。必须把所有的“读”操作集中在一起,所有的“写”操作集中在一起放到 rAF 里执行。
- 如果你在滑动时,先读取一下
5. 终极避坑(面试加分项):图层爆炸 (Layer Explosion)
如果你回答得太好,面试官会抛出最后的杀手锏:硬件加速有什么副作用吗?能滥用吗?
- 副作用:每一个独立的 GPU 合成层都会消耗额外的内存。如果给页面上所有的元素都加上
will-change: transform,会导致图层爆炸。手机内存吃紧,反而会让整个页面卡死甚至闪退。 - Z-index 陷阱:如果一个
z-index较低的元素被提升到了 GPU 独立图层,为了保证层叠顺序的正确性,浏览器会被迫把覆盖在它上面的所有z-index较高的普通元素也提升为独立图层(隐式合成)。这也是导致图层爆炸的常见原因。 - 如何治理:动画开始前加上
will-change,动画结束后立刻通过 JS 将其移除(element.style.willChange = 'auto'),把图层交还给浏览器释放内存。