动画性能优化
问题
前端如何实现高性能动画?CSS 动画和 JavaScript 动画有什么区别?什么是 GPU 加速?
面试速答版
前端如何实现高性能动画? 目标是稳定 60fps,每帧只有 16.67ms 预算:
- 只动
transform和opacity——这两个属性只触发 Composite,由 GPU 处理,不占主线程。别用width/left/margin这些会触发 Layout 的属性。 - 用
requestAnimationFrame替代setTimeout/setInterval,跟浏览器刷新率同步,避免掉帧。 - 复杂计算丢给 Web Worker,避免主线程被 JS 阻塞导致掉帧。
- 用 DevTools Performance 面板录制,看 FPS 曲线和帧详情,超过 16.67ms 的帧会标红。
CSS 动画和 JavaScript 动画有什么区别? 两者各有适用场景,不是「谁更好」而是「什么时候用谁」:
- CSS 动画(
transition/@keyframes):声明式、引擎可优化、能直接走 GPU 合成层;适合简单状态切换、hover 效果、loading。缺点是动态值难处理、暂停恢复麻烦。 - JS 动画(
requestAnimationFrame/WAAPI):完全可控、能根据数据/事件实时调整、容易做缓动、暂停、反向;适合复杂交互动画、物理动效、需要根据用户输入响应的场景。 - Web Animations API(
element.animate()) 是折中方案:JS 写法但运行在合成线程,性能接近 CSS。 - 一般原则:能用 CSS 就用 CSS,复杂逻辑用 WAAPI 或 Framer Motion / GSAP。
什么是 GPU 加速? 指把元素提升为独立的合成层(Compositing Layer),由 GPU 单独绘制和移动:
- 触发方式:
transform: translateZ(0)/translate3d(0,0,0)/will-change: transform/backface-visibility: hidden。 - 好处:动画不再走主线程的 Layout/Paint,直接 GPU 合成,能跑出极致 60fps。
- 代价:每个合成层都吃显存,移动端尤其敏感——不要给
*加will-change,会让浏览器创建大量层、吃光显存反而更卡。 - 正确用法:动画开始前加
will-change: transform,动画结束后移除(设回auto),按需启用。
答案
流畅的动画需要达到 60fps(每帧约 16.67ms)。动画性能优化的核心是减少主线程工作,利用 GPU 加速,避免重排重绘。
动画性能基础
浏览器渲染流水线
| 阶段 | 工作 | 耗时 | 优化目标 |
|---|---|---|---|
| JavaScript | 动画逻辑 | 高 | 减少计算量 |
| Style | 计算样式 | 中 | 减少选择器复杂度 |
| Layout | 布局计算 | 高 | 避免触发 |
| Paint | 绘制像素 | 中 | 避免触发 |
| Composite | 合成图层 | 低 | 只触发这步 |
动画帧时间预算
60fps = 1000ms / 60 = 16.67ms 每帧
帧时间预算分配:
├── JavaScript: ~3ms
├── Style: ~1ms
├── Layout: ~2ms(避免)
├── Paint: ~2ms(避免)
├── Composite: ~1ms
└── 浏览器开销: ~2ms
剩余缓冲: ~5ms
CSS 动画 vs JavaScript 动画
| 特性 | CSS 动画 | JavaScript 动画 |
|---|---|---|
| 性能 | GPU 加速 | 主线程执行 |
| 控制性 | 有限 | 完全控制 |
| 复杂动画 | 困难 | 灵活 |
| 暂停/恢复 | CSS 变量 | 容易 |
| 动态值 | 不支持 | 支持 |
| 调试 | 困难 | 容易 |
CSS 动画最佳实践
/* ✅ 使用 transform 和 opacity - 只触发 Composite */
.box {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.box:hover {
transform: translateX(100px) scale(1.1);
opacity: 0.8;
}
/* ❌ 避免使用这些属性 - 触发 Layout/Paint */
.box-bad:hover {
width: 200px; /* Layout */
height: 200px; /* Layout */
margin-left: 50px; /* Layout */
background: red; /* Paint */
box-shadow: 0 0 10px; /* Paint */
}
CSS 关键帧动画
/* GPU 加速动画 */
@keyframes slideIn {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.animated {
animation: slideIn 0.5s ease-out forwards;
/* 提示浏览器开启 GPU 加速 */
will-change: transform, opacity;
}
/* 动画结束后移除 will-change */
.animated.finished {
will-change: auto;
}
GPU 加速属性
只触发 Composite 的属性
| 属性 | 说明 | GPU 加速 |
|---|---|---|
| transform: translate | 位移 | ✅ |
| transform: scale | 缩放 | ✅ |
| transform: rotate | 旋转 | ✅ |
| transform: skew | 倾斜 | ✅ |
| opacity | 透明度 | ✅ |
| filter | 滤镜(部分) | ✅ |
will-change 使用
/* ✅ 正确使用:在动画开始前添加 */
.box {
will-change: transform;
}
/* ❌ 避免滥用 */
* {
will-change: transform, opacity; /* 消耗大量内存 */
}
/* ✅ 动态添加/移除 */
.box:hover {
will-change: transform;
}
.box:active {
transform: scale(1.1);
}
强制创建合成层
/* 创建独立合成层 */
.gpu-layer {
transform: translateZ(0);
/* 或 */
will-change: transform;
/* 或 */
backface-visibility: hidden;
}
requestAnimationFrame
requestAnimationFrame 是 JavaScript 动画的最佳选择,它与浏览器刷新率同步。
// 基础用法
function animate() {
// 更新动画状态
element.style.transform = `translateX(${x}px)`;
// 继续下一帧
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
// 带时间控制
function animateWithTime(timestamp: number) {
if (!startTime) startTime = timestamp;
const progress = (timestamp - startTime) / duration;
if (progress < 1) {
const value = easeOut(progress);
element.style.transform = `translateX(${value * 100}px)`;
requestAnimationFrame(animateWithTime);
}
}
封装动画函数
interface AnimationOptions {
duration: number;
easing?: (t: number) => number;
onUpdate: (progress: number) => void;
onComplete?: () => void;
}
function animate({
duration,
easing = (t) => t, // 线性
onUpdate,
onComplete,
}: AnimationOptions): () => void {
let startTime: number | null = null;
let rafId: number;
function frame(timestamp: number) {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / duration, 1);
const easedProgress = easing(progress);
onUpdate(easedProgress);
if (progress < 1) {
rafId = requestAnimationFrame(frame);
} else {
onComplete?.();
}
}
rafId = requestAnimationFrame(frame);
// 返回取消函数
return () => cancelAnimationFrame(rafId);
}
// 常用缓动函数
const easings = {
linear: (t: number) => t,
easeIn: (t: number) => t * t,
easeOut: (t: number) => t * (2 - t),
easeInOut: (t: number) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
easeOutElastic: (t: number) => {
const c4 = (2 * Math.PI) / 3;
return t === 0 ? 0 : t === 1 ? 1
: Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
},
};
// 使用示例
const cancel = animate({
duration: 1000,
easing: easings.easeOut,
onUpdate: (progress) => {
element.style.transform = `translateX(${progress * 200}px)`;
},
onComplete: () => {
console.log('Animation complete');
},
});
Web Animations API
Web Animations API (WAAPI) 结合了 CSS 动画和 JavaScript 控制的优点。
// 基础用法
const animation = element.animate(
[
{ transform: 'translateX(0)', opacity: 1 },
{ transform: 'translateX(100px)', opacity: 0.5 }
],
{
duration: 1000,
easing: 'ease-out',
fill: 'forwards',
}
);
// 控制动画
animation.play();
animation.pause();
animation.reverse();
animation.cancel();
// 监听事件
animation.onfinish = () => console.log('Finished');
animation.oncancel = () => console.log('Cancelled');
// 异步等待
await animation.finished;
复杂动画编排
// 序列动画
async function sequenceAnimation(elements: HTMLElement[]) {
for (const element of elements) {
await element.animate(
[
{ transform: 'scale(0)', opacity: 0 },
{ transform: 'scale(1)', opacity: 1 }
],
{ duration: 300, fill: 'forwards' }
).finished;
}
}
// 并行动画
function parallelAnimation(elements: HTMLElement[]) {
return Promise.all(
elements.map(element =>
element.animate(
[
{ transform: 'translateY(-20px)', opacity: 0 },
{ transform: 'translateY(0)', opacity: 1 }
],
{ duration: 300, fill: 'forwards' }
).finished
)
);
}
// 交错动画
function staggerAnimation(elements: HTMLElement[], stagger = 50) {
elements.forEach((element, index) => {
element.animate(
[
{ transform: 'translateY(-20px)', opacity: 0 },
{ transform: 'translateY(0)', opacity: 1 }
],
{
duration: 300,
delay: index * stagger,
fill: 'forwards',
}
);
});
}
动画性能分析
Chrome DevTools Performance
// 使用 Performance API 测量
performance.mark('animation-start');
// 执行动画...
performance.mark('animation-end');
performance.measure('animation', 'animation-start', 'animation-end');
const measure = performance.getEntriesByName('animation')[0];
console.log(`Animation took ${measure.duration}ms`);
帧率监控
class FPSMonitor {
private frames = 0;
private lastTime = performance.now();
private fps = 0;
private rafId: number | null = null;
start() {
const loop = (time: number) => {
this.frames++;
if (time - this.lastTime >= 1000) {
this.fps = this.frames;
this.frames = 0;
this.lastTime = time;
this.onFPSUpdate(this.fps);
}
this.rafId = requestAnimationFrame(loop);
};
this.rafId = requestAnimationFrame(loop);
}
stop() {
if (this.rafId) {
cancelAnimationFrame(this.rafId);
}
}
private onFPSUpdate(fps: number) {
console.log(`FPS: ${fps}`);
if (fps < 50) {
console.warn('Low frame rate detected');
}
}
}
检测动画卡顿
function detectJank() {
let lastFrameTime = performance.now();
let jankCount = 0;
function frame(time: number) {
const delta = time - lastFrameTime;
// 超过 50ms 认为卡顿(低于 20fps)
if (delta > 50) {
jankCount++;
console.warn(`Jank detected: ${delta.toFixed(2)}ms frame`);
}
lastFrameTime = time;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
return () => jankCount;
}
React 动画优化
import { useRef, useEffect, useCallback } from 'react';
// 使用 CSS + className
function CSSAnimation() {
const [isVisible, setIsVisible] = useState(false);
return (
<div className={`box ${isVisible ? 'visible' : ''}`}>
Content
</div>
);
}
// 使用 WAAPI
function WAAPIAnimation() {
const ref = useRef<HTMLDivElement>(null);
const animate = useCallback(() => {
ref.current?.animate(
[
{ transform: 'scale(1)' },
{ transform: 'scale(1.1)' },
{ transform: 'scale(1)' },
],
{ duration: 200, easing: 'ease-in-out' }
);
}, []);
return <div ref={ref} onClick={animate}>Click me</div>;
}
// 使用 framer-motion
import { motion, useAnimationControls } from 'framer-motion';
function FramerMotionExample() {
const controls = useAnimationControls();
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
Content
</motion.div>
);
}
常见优化技巧
1. 使用 transform 代替位置属性
/* ❌ 触发 Layout */
.bad {
left: 100px;
top: 50px;
}
/* ✅ 只触发 Composite */
.good {
transform: translate(100px, 50px);
}
2. 避免动画期间读取布局
// ❌ 强制同步布局
function animateBad(element: HTMLElement) {
const height = element.offsetHeight; // 强制布局
element.style.height = `${height + 10}px`; // 触发布局
}
// ✅ 批量读写
function animateGood(element: HTMLElement) {
// 批量读取
const height = element.offsetHeight;
// 使用 RAF 批量写入
requestAnimationFrame(() => {
element.style.height = `${height + 10}px`;
});
}
3. 使用 CSS 变量动态更新
.box {
--x: 0;
--y: 0;
transform: translate(var(--x), var(--y));
}
// 高效更新
function updatePosition(element: HTMLElement, x: number, y: number) {
element.style.setProperty('--x', `${x}px`);
element.style.setProperty('--y', `${y}px`);
}
4. 使用 contain 隔离重绘
.animated-card {
/* 隔离重绘影响 */
contain: layout style paint;
}
常见面试问题
Q1: CSS 动画和 JS 动画的区别?
答案:
| 特性 | CSS 动画 | JS 动画 |
|---|---|---|
| 执行线程 | 合成线程(GPU) | 主线程 |
| 性能 | 更好 | 可能阻塞 |
| 控制性 | 有限 | 完全控制 |
| 兼容性 | 好 | 需要 polyfill |
使用建议:
- 简单动画 → CSS transition/animation
- 复杂交互 → WAAPI 或 framer-motion
- 游戏/高频更新 → Canvas/WebGL
Q2: 什么是 GPU 加速?哪些属性可以 GPU 加速?
答案:
GPU 加速是将元素提升到独立合成层,由 GPU 处理渲染。
/* GPU 加速属性 */
transform: translate/scale/rotate
opacity
filter
/* 触发 GPU 加速 */
.element {
will-change: transform;
/* 或 */
transform: translateZ(0);
}
Q3: requestAnimationFrame 和 setTimeout 的区别?
答案:
| 特性 | requestAnimationFrame | setTimeout |
|---|---|---|
| 执行时机 | 下次重绘前 | 指定延迟后 |
| 帧率 | 与刷新率同步 | 不稳定 |
| 后台标签 | 暂停 | 继续执行 |
| 精度 | 高 | 可能有偏差 |
// RAF - 60fps
function animate() {
// 每帧执行一次,与显示器刷新同步
requestAnimationFrame(animate);
}
// setTimeout - 可能卡顿
function animateBad() {
// 可能与刷新率不同步,导致丢帧
setTimeout(animateBad, 16);
}
Q4: will-change 的作用和注意事项?
答案:
/* 作用:提示浏览器准备动画优化 */
.box {
will-change: transform, opacity;
}
注意事项:
- 不要滥用:每个元素都创建合成层会消耗内存
- 动画前添加,动画后移除
- 不要用于静态元素
- 不要用在太多元素上
Q5: 如何实现 60fps 流畅动画?
答案:
// 1. 使用 transform 和 opacity
element.style.transform = 'translateX(100px)';
// 2. 使用 requestAnimationFrame
function animate() {
updateAnimation();
requestAnimationFrame(animate);
}
// 3. 避免强制同步布局
const rect = element.getBoundingClientRect();
requestAnimationFrame(() => {
element.style.transform = `translateX(${rect.x}px)`;
});
// 4. 减少 DOM 操作
// 使用 CSS 变量批量更新
// 5. 使用 Web Workers 处理计算
// 复杂计算在 Worker 中进行