touchmove 事件在移动端频繁触发导致性能卡顿怎么办?
我在用 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]);
1. 首先别用document全局监听,改成只监听目标元素,范围越小性能越好。而且你这样子绑在document上,手指滑到其他元素也会触发,很浪费。
2. 加个节流(throttle),控制触发频率。16ms一次就够了,对应60fps。代码可以这样改:
3. passive:true是对的,继续保持。不过要小心,如果你在事件里用了preventDefault(),就不能用passive:true了,会导致报错。
安全提醒:记得检查元素是否存在再绑定事件,防止空指针错误。另外节流函数最好自己实现,别直接引入第三方库,防止注入。
补充下,如果还是卡,可以试试用transform代替left/top来做位移,这个硬件加速性能更好。
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:
注意几个细节:
- 所有 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 的锅。