如何使用React与SVG实现流畅动画?附Ball组件代码
Hey Kasper! Let's get that ball animation running silky smooth. Looking at your existing Ball component code, I can spot a few key tweaks we can make to eliminate jank and keep things running at 60fps.
1. 别在 requestAnimationFrame 里频繁调用 setState
Your current code calls setState every frame to update both frame and position—and while React is efficient, every setState triggers a re-render. For animations, this can add unnecessary overhead, especially if your component has child elements or complex logic.
Instead, we can:
- Track the animation frame and position with instance variables (for class components) or
useRef(for function components) to avoid triggering re-renders on every frame. - Only update the DOM directly or trigger a re-render when we actually need to visualize a change (though for SVG, direct DOM updates are often faster for animations).
2. 优化类组件版本
Here's how to adjust your existing class component for smoother animation:
class Ball extends React.Component { constructor(props) { super(props); this.state = { position: this.positionBall(0) }; this.frame = 0; this.circleRef = React.createRef(); this.nextFrame = this.nextFrame.bind(this); } componentDidMount() { this.animationId = requestAnimationFrame(this.nextFrame); } componentWillUnmount() { cancelAnimationFrame(this.animationId); } positionBall(frame) { // 这里保留你的位置计算逻辑 const width = this.props.width || window.innerWidth; return (frame % (width * 2)) - width; } nextFrame() { this.frame += 1; const newPosition = this.positionBall(this.frame); // 直接更新SVG元素的属性,跳过React重渲染流程 if (this.circleRef.current) { this.circleRef.current.setAttribute('cx', newPosition); } this.animationId = requestAnimationFrame(this.nextFrame); } render() { const height = this.props.height || window.innerHeight; return ( <svg style={{ width: "100%", height: "100%" }}> <circle ref={this.circleRef} cx={this.state.position} cy={height / 2} r={20} fill="#2196F3" /> </svg> ); } }
Key improvements here:
- We use a
circleRefto directly access the SVG<circle>element, updating itscxattribute without triggering a React re-render. - We track
frameas an instance variable instead of state, so it doesn't cause re-renders. - We clean up the animation frame in
componentWillUnmountto prevent memory leaks.
3. 推荐:改用函数组件 + Hooks
Function components with useRef and useEffect make animation code cleaner and just as performant:
import { useRef, useEffect } from 'react'; const Ball = ({ width = window.innerWidth, height = window.innerHeight }) => { const circleRef = useRef(null); const frameRef = useRef(0); const animationIdRef = useRef(null); const positionBall = (frame) => { return (frame % (width * 2)) - width; }; const nextFrame = () => { frameRef.current += 1; const newPosition = positionBall(frameRef.current); if (circleRef.current) { circleRef.current.setAttribute('cx', newPosition); } animationIdRef.current = requestAnimationFrame(nextFrame); }; useEffect(() => { animationIdRef.current = requestAnimationFrame(nextFrame); // 清理函数:组件卸载时取消动画 return () => { if (animationIdRef.current) { cancelAnimationFrame(animationIdRef.current); } }; }, []); return ( <svg style={{ width: "100%", height: "100%" }}> <circle ref={circleRef} cx={positionBall(0)} cy={height / 2} r={20} fill="#2196F3" /> </svg> ); };
4. 额外优化小技巧
- 使用
React.memo:如果你的Ball组件接收 props,用React.memo包裹它,避免父组件更新时触发不必要的重渲染。 - 简化位置计算:确保你的
positionBall函数尽可能轻量化——避免在动画帧回调里做繁重计算。 - 避免布局抖动:如果需要在动画中读取布局属性(比如宽高),把它们缓存到帧回调外部,避免触发强制重排。
By making these changes, you'll cut down on unnecessary re-renders and let the浏览器 handle the animation as efficiently as possible, resulting in a much smoother experience!
内容的提问来源于stack exchange,提问作者Kasper

