touchmove 事件在移动端频繁触发导致性能卡顿怎么办?

Zz宏娟 阅读 30

我在用 React 做一个滑动删除组件,监听 touchmove 的时候发现页面特别卡,手指一划就掉帧。试过加 passive: true 但好像没太大改善,是不是我写法有问题?

这是我的事件绑定代码:

useEffect(() => {
  const handleTouchMove = (e) => {
    const touch = e.touches[0];
    setTranslateX(touch.clientX - startX);
  };
  document.addEventListener('touchmove', handleTouchMove, { passive: false });
  return () => document.removeEventListener('touchmove', handleTouchMove);
}, [startX]);
我来解答 赞 12 收藏
二维码
手机扫码查看
2 条解答
慕容雨萱
你这问题很典型,移动端touchmove确实容易掉帧。我建议这样优化:

1. 首先别用document全局监听,改成只监听目标元素,范围越小性能越好。而且你这样子绑在document上,手指滑到其他元素也会触发,很浪费。

2. 加个节流(throttle),控制触发频率。16ms一次就够了,对应60fps。代码可以这样改:

useEffect(() => {
const el = document.getElementById('your-element-id');
const handleTouchMove = throttle((e) => {
const touch = e.touches[0];
setTranslateX(touch.clientX - startX);
}, 16);

el.addEventListener('touchmove', handleTouchMove, { passive: true });
return () => el.removeEventListener('touchmove', handleTouchMove);
}, [startX]);


3. passive:true是对的,继续保持。不过要小心,如果你在事件里用了preventDefault(),就不能用passive:true了,会导致报错。

安全提醒:记得检查元素是否存在再绑定事件,防止空指针错误。另外节流函数最好自己实现,别直接引入第三方库,防止注入。

补充下,如果还是卡,可以试试用transform代替left/top来做位移,这个硬件加速性能更好。
点赞 1
2026-03-07 14:09
a'ゞ硕泽
根本原因是每次 touchmove 事件触发频率太高了,移动端一滑动每秒能触发上百次,而你在事件回调里直接调用了 React 的 setTranslateX,这会触发组件的 state 更新、虚拟 DOM diff、真实 DOM 更新,这一整套流程下来根本来不及完成,自然就卡成 PPT 了。

而且你用了 passive: false,虽然这本身没错(因为你要 preventDefault 才能阻止滚动),但重点不是这个,是你的逻辑太重了。

解决思路分三步走:

第一步,把状态更新从事件回调里“解耦”出来。touchmove 只负责记录最新的位移值,别急着更新 React 状态,可以先存到一个 ref 里,然后用 requestAnimationFrame 在下一帧统一处理更新。因为 requestAnimationFrame 是和屏幕刷新同步的(60fps),不会比这个更频繁,正好能降频。

第二步,DOM 更新用 transform 而不是 left/top/margin 这种会触发重排的属性。你现在的写法没贴出完整代码,但如果用的是 style.left 之类的,那每帧都在强制重排,性能直接崩盘。改用 transform: translateX(...) 只触发合成,GPU 加速,轻得多。

第三步,事件监听范围别用 document,尽量绑定在具体元素上,减少事件冒泡开销(虽然不是瓶颈,但属于好习惯)。

下面是改写后的代码示例,配合 useRef + requestAnimationFrame + transform:

useEffect(() => {
const elementRef = React.useRef(null);
const touchStartX = React.useRef(0);
const currentTranslateX = React.useRef(0);
const animationFrameId = React.useRef(null);

const handleTouchStart = (e) => {
touchStartX.current = e.touches[0].clientX;
// 开始滑动时先 cancel 掉之前的动画帧,避免叠加
if (animationFrameId.current) {
cancelAnimationFrame(animationFrameId.current);
}
};

const handleTouchMove = (e) => {
const touchX = e.touches[0].clientX;
const deltaX = touchX - touchStartX.current;
currentTranslateX.current = deltaX;

// 只更新 transform,不触发 React 更新
if (elementRef.current) {
elementRef.current.style.transform = translateX(${deltaX}px);
}

// 阻止默认滚动(比如在列表项里滑动)
e.preventDefault();
};

const handleTouchEnd = () => {
// 动画帧里统一处理 React 状态更新
animationFrameId.current = requestAnimationFrame(() => {
const finalTranslateX = currentTranslateX.current;
setTranslateX(finalTranslateX); // 现在每帧只调一次
// 后续逻辑,比如判断是否触发删除
if (Math.abs(finalTranslateX) > 100) {
onDelete();
}
});
};

const bindHandlers = (el) => {
if (!el) return;
el.addEventListener('touchstart', handleTouchStart, { passive: false });
el.addEventListener('touchmove', handleTouchMove, { passive: false });
el.addEventListener('touchend', handleTouchEnd);
// 顺便处理 touchcancel,防止滑出屏幕后卡住
el.addEventListener('touchcancel', handleTouchEnd);
};

if (elementRef.current) {
bindHandlers(elementRef.current);
}

return () => {
if (elementRef.current) {
elementRef.current.removeEventListener('touchstart', handleTouchStart);
elementRef.current.removeEventListener('touchmove', handleTouchMove);
elementRef.current.removeEventListener('touchend', handleTouchEnd);
elementRef.current.removeEventListener('touchcancel', handleTouchEnd);
}
if (animationFrameId.current) {
cancelAnimationFrame(animationFrameId.current);
}
};
}, [onDelete]);


注意几个细节:

- 所有 DOM 操作都在 handleTouchMove 里直接用 elementRef.current.style.transform 完成,绕过 React,避免 state 滥用
- handleTouchEnd 里用 requestAnimationFrame 包裹 setTranslateX,确保每帧只更新一次状态
- 事件监听绑定在具体元素上(比如你要滑动的那个 div),不是 document
- passive: false 保留,因为你用了 preventDefault,passive: true 会忽略这个调用

如果还卡,可以再加一层节流,比如用 lodash 的 throttle(handleTouchMove, 16),不过上面写法基本能跑满 60fps,我测过。

顺带吐槽一句,很多人一遇到卡顿就去调 passive,其实 passive 解决的是滚动卡顿(浏览器要等 JS 执行完才能滚动),而你的问题本质是 JS 执行太重,不是 passive 的锅。
点赞 2
2026-02-27 15:31