问题背景
抖音数据大屏用了大量 Framer Motion 动画:数字滚动、地图下钻过渡、卡片悬浮效果。开发环境跑得挺好,但部署到客户老电脑上,风扇狂转,帧率掉到 30fps 以下。
性能分析
用 Chrome DevTools 的 Performance 面板录了一波,发现两个主要问题:
- animate 触发了 layout:改了 width/height/top/left 这些触发 layout 的属性
- 同时动画元素太多:地图下钻时,全国3000+区县同时animate,直接炸了
解决方案
1. 只用 transform 和 opacity
这两个属性不会触发 layout/paint,只触发 composite,GPU 直接处理:
// 错误:改 width/height,触发 layout
<motion.div
animate={{ width: isOpen ? 200 : 0 }}
/>
// 正确:用 transform: scaleX
<motion.div
animate={{ scaleX: isOpen ? 1 : 0 }}
style={{ transformOrigin: "left" }}
/>
// 错误:改 top/left
animate={{ top: y, left: x }}
// 正确:用 transform
animate={{ x, y }}
2. 用 willChange: "transform" 提前告诉浏览器
<motion.div
style={{ willChange: "transform", transform: "translateZ(0)" }}
animate={{ x: 100 }}
/>
// translateZ(0) 触发硬件加速,提前创建合成层
3. 懒加载 + 虚拟滚动处理大数据量动画
地图下钻时不给所有区县同时加动画,只给可视区域内的加:
// 用 IntersectionObserver 判断是否在视口内
const [visible, setVisible] = useState(false);
const ref = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => setVisible(entry.isIntersecting),
{ threshold: 0.1 }
);
observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return (
<motion.div
ref={ref}
animate={visible ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
);
4. 用 transition={{ type: false }} 禁用物理弹簧
Framer Motion 默认用 spring 动画,物理计算很耗性能。数据大屏不需要弹性效果,用 tween 就行:
// 错误:默认 spring,性能开销大
<motion.div animate={{ x: 100 }} />
// 正确:用 tween,性能更好
<motion.div
animate={{ x: 100 }}
transition={{ type: "tween", duration: 0.3, ease: "easeOut" }}
/>
5. 用 useTransform 和 useSpring 代替 animate
对于高频更新的动画(比如实时数据滚动),用 Framer Motion 的 value API 直接操作,跳过 React 渲染周期:
import { useSpring, useTransform } from "framer-motion";
function AnimatedNumber({ value }) {
const springValue = useSpring(0, { stiffness: 300, damping: 30 });
const display = useTransform(springValue, v => Math.round(v));
useEffect(() => {
springValue.set(value);
}, [value]);
return <motion.span>{display}</motion.span>;
}
✅ 最终效果:优化后,同时动画元素从 3000+ 降到视口内约 50 个,帧率稳定在 58-60fps,客户老电脑也不卡了。
快速检查清单
- 所有动画是否只用
transform和opacity? - 是否给动画元素加了
will-change: transform? - 大量元素的动画是否用了虚拟化/懒加载?
- 是否把默认的 spring 改成了 tween?
- 高频更新是否用了
useSpring而不是animate?