跳到主要内容

动画性能优化

问题

前端如何实现高性能动画?CSS 动画和 JavaScript 动画有什么区别?什么是 GPU 加速?

答案

流畅的动画需要达到 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 的区别?

答案

特性requestAnimationFramesetTimeout
执行时机下次重绘前指定延迟后
帧率与刷新率同步不稳定
后台标签暂停继续执行
精度可能有偏差
// RAF - 60fps
function animate() {
// 每帧执行一次,与显示器刷新同步
requestAnimationFrame(animate);
}

// setTimeout - 可能卡顿
function animateBad() {
// 可能与刷新率不同步,导致丢帧
setTimeout(animateBad, 16);
}

Q4: will-change 的作用和注意事项?

答案

/* 作用:提示浏览器准备动画优化 */
.box {
will-change: transform, opacity;
}

注意事项

  1. 不要滥用:每个元素都创建合成层会消耗内存
  2. 动画前添加,动画后移除
  3. 不要用于静态元素
  4. 不要用在太多元素上

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 中进行

相关链接