React用requestAnimationFrame做滚动动画移动端卡顿怎么办?

Air-雯清 阅读 35

在移动端用React写了一个根据滚动位置变化的动画组件,用requestAnimationFrame更新状态,但手机上滑动时动画特别卡,帧率掉到20多…

代码逻辑是监听scroll事件,在useEffect里用rAF更新位置数据。尝试过给scroll事件加节流函数,但效果不大。测试发现每次rAF回调里会重新计算复杂的CSS变量,这样是不是性能瓶颈?


function ScrollAnim() {
  const [pos, setPos] = useState(0);
  useEffect(() => {
    const handleScroll = () => requestAnimationFrame(() => {
      setPos(window.scrollY);
      // 这里计算多个变形属性
      document.body.style.setProperty('--anim-scale', <code>${1 + pos*0.001}</code>);
    });
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);
  return <div className="animated-element" />;
}

移动端测试时发现滚动越快卡顿越明显,是不是不应该直接在rAF里修改DOM属性?或者应该用Web Animation API代替?

我来解答 赞 8 收藏
二维码
手机扫码查看
2 条解答
令狐梦幻
移动端卡顿主要是因为频繁操作 DOM 和样式计算导致的性能问题。你提到每次 rAF 中都在改 body 的 style,这会强制浏览器同步重排重绘,影响性能。

可以试试这样优化:
把 CSS 变量计算换成 transform 等合成器线程处理的属性,减少重排
用 useRef 缓存上次的 pos 值,避免频繁触发 setPos 和重渲染
用 translateZ 或 will-change 提升动画元素层级,启用 GPU 加速

比如:
function ScrollAnim() {
const posRef = useRef(0);
const tickingRef = useRef(false);

useEffect(() => {
const handleScroll = () => {
if (!tickingRef.current) {
requestAnimationFrame(() => {
const newPos = window.scrollY;
posRef.current = newPos;

// 改成 transform
document.body.style.transform = scale(${1 + newPos * 0.001});

tickingRef.current = false;
});
tickingRef.current = true;
}
};

window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);

return
;
}


另外还可以考虑用 Web Animation API 替代直接操作 style,让浏览器自己优化动画管线。
点赞 6
2026-02-05 14:11
Zz慧丽
Zz慧丽 Lv1
我也在这坑里躺过,这问题我来总结几点血泪教训。

你的问题主要有两个点导致卡顿:

1. requestAnimationFrame 里频繁操作 DOM 设置 CSS 变量,本身就容易引发强制同步布局,尤其是在滚动这种高频场景下。移动端性能本就有限,每次滚动都改多个样式属性,很容易造成帧率暴跌。

2. setPos(window.scrollY) 每次滚动都会触发状态更新,进而导致 React 组件重新渲染,这在移动端是灾难级的性能消耗。

### 解决方案:

#### ✅ 1. **避免频繁触发 React 状态更新**
滚动动画尽量不要用 React 的 state 来驱动,而是直接通过 DOM 操作处理。动画状态不需要驱动组件树更新,用局部变量即可:

let lastScrollY = 0;


#### ✅ 2. **避免在 rAF 中反复操作 DOM 样式**
CSS 变量更新可以缓存判断,只有当值真正变化时才去设置:

let lastScale = -1;
function updateAnimation() {
const currentScale = 1 + window.scrollY * 0.001;
if (currentScale !== lastScale) {
document.body.style.setProperty('--anim-scale', currentScale);
lastScale = currentScale;
}
}


#### ✅ 3. **滚动监听加节流 + rAF 控制更新频率**
节流函数 + requestAnimationFrame 双重控制,避免频繁触发:

useEffect(() => {
let ticking = false;

const handleScroll = () => {
if (!ticking) {
requestAnimationFrame(() => {
updateAnimation();
ticking = false;
});
ticking = true;
}
};

window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);


#### ✅ 4. 如果动画足够复杂,**建议上 Web Animation API**
这个 API 是浏览器原生支持的动画系统,性能更好,能交由合成线程处理,不占用主线程太多资源:

const anim = document.querySelector('.animated-element').animate([
{ transform: 'scale(1)' },
{ transform: 'scale(1.5)' }
], {
duration: 1000,
fill: 'forwards'
});


不过 Web Animation API 的控制粒度比直接计算 scrollY 来得粗,需要根据具体动画场景取舍。

---

### 最后建议:
移动端做滚动动画,核心就是「**能不动就不动,能少动就少动**」。我当时也是滚动越快越卡,优化完帧率从20多干到了60,体验提升非常大。

如果还有卡顿,可以考虑用 transform: translate3d 这类 GPU 加速的属性,而不是靠 JS 精确控制每个像素点的动画。
点赞 10
2026-02-04 18:50