You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React自定义useDrag Hook触发mousemove事件导致页面渲染卡顿如何优化?

现有实现的核心问题
  • 你使用useState存储dragStart和dragEnd,每次mousemove触发setDragEnd都会强制调用useDrag的父组件全量重渲染,mousemove每秒触发60+次,相当于每秒触发60+次组件重渲染,自然会出现卡顿甚至冻结
  • useEffect依赖项包含dragStart,每次拖拽开始/结束都会重新绑定一遍事件监听器,产生冗余操作
  • 拖拽选框的样式更新走React状态渲染链路,性能开销远高于直接操作DOM
可行优化方案
  • 拖拽过程中的临时坐标改用useRef存储,更新ref不会触发组件重渲染,仅在拖拽结束需要提交结果时再更新状态
  • 用requestAnimationFrame控制选框样式更新频率,和浏览器刷新率对齐,避免无效的高频更新
  • 事件监听器绑定到document对象上,避免鼠标移出绑定区域后拖拽中断,同时优化useEffect依赖项,减少重复绑定
  • 若确实需要在父组件获取实时拖拽坐标,可添加节流逻辑,控制状态更新频率不超过60次/秒
  • 如果仅需要渲染拖拽选框,可将选框DOM操作封装在useDrag内部,完全绕开React状态渲染链路,性能最优
优化后代码示例
import { useRef, useState, useEffect } from 'react';

// 先定义Coordinates类型
type Coordinates = { x: number; y: number } | null;

// 辅助函数 获取鼠标相对于元素的偏移
const getMouseOffset = (e: MouseEvent, element: HTMLElement | null): Coordinates => {
  if (!element) return null;
  const rect = element.getBoundingClientRect();
  return {
    x: e.clientX - rect.left,
    y: e.clientY - rect.top,
  };
};

export const useDrag = (selectionRef: React.MutableRefObject<HTMLDivElement | null>) => {
  // 临时坐标存在ref里 不触发重渲染
  const dragStartRef = useRef<Coordinates>(null);
  const dragEndRef = useRef<Coordinates>(null);
  // 只有需要对外暴露的状态才用useState 可以按需控制更新频率
  const [pendingSelection, setPendingSelection] = useState<{ start: Coordinates; end: Coordinates } | null>(null);
  // raf id用来取消帧回调
  const rafIdRef = useRef<number | null>(null);

  const onMouseDown = (e: MouseEvent) => {
    dragStartRef.current = getMouseOffset(e, selectionRef.current);
  };

  const onMouseUp = () => {
    dragStartRef.current = null;
    dragEndRef.current = null;
    setPendingSelection(null);
    if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current);
  };

  const onMouseMove = (e: MouseEvent) => {
    if (!dragStartRef.current) return;
    dragEndRef.current = getMouseOffset(e, selectionRef.current);
    // 用raf控制更新频率 对齐浏览器刷新率
    if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current);
    rafIdRef.current = requestAnimationFrame(() => {
      if (dragStartRef.current && dragEndRef.current) {
        setPendingSelection({
          start: dragStartRef.current,
          end: dragEndRef.current,
        });
      }
    });
  };

  useEffect(() => {
    // 事件绑定到document 避免拖拽移出元素失效
    document.addEventListener('mousedown', onMouseDown);
    document.addEventListener('mouseup', onMouseUp);
    document.addEventListener('mousemove', onMouseMove);
    return () => {
      document.removeEventListener('mousedown', onMouseDown);
      document.removeEventListener('mouseup', onMouseUp);
      document.removeEventListener('mousemove', onMouseMove);
      if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current);
    };
  }, []); // 依赖项为空 只绑定一次

  return { pendingSelection };
};

如果性能要求极高,不需要在父组件使用pendingSelection做其他逻辑,仅用于渲染选框,可以进一步改造为直接操作DOM,完全不需要状态触发重渲染:

// 调用侧代码
const selectionRef = useRef<HTMLDivElement>(null);
const selectBoxRef = useRef<HTMLDivElement>(null);
// 改造useDrag传入selectBoxRef,内部直接修改style.left/top/width/height属性即可

return (
  <div className="selection" ref={selectionRef} style={{ position: 'relative' }}>
    <div ref={selectBoxRef} style={{ position: 'absolute', background: 'rgba(0,0,0,0.1)', pointerEvents: 'none' }}></div>
  </div>
);

内容的提问来源于stack exchange,提问作者Michael Sanchez

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.10 03:15:02