React / Next.js

React 日历拖拽的性能排查

排期日历常同时展示日期、票型、库存和价格。拖拽时鼠标每移动一次就更新整个页面状态,容易让大量单元格重复渲染。优化前先确认慢在哪里:浏览器绘制、数据计算、React 提交,还是请求返回。

排期日历常同时展示日期、票型、库存和价格。拖拽时鼠标每移动一次就更新整个页面状态,容易让大量单元格重复渲染。优化前先确认慢在哪里:浏览器绘制、数据计算、React 提交,还是请求返回。

先测量,再定位

用 React Profiler 录制一次完整拖拽,重点看每次提交耗时、哪些组件重复渲染、以及拖拽之外的列表是否被带着更新。再用浏览器 Performance 面板对照长任务、布局和绘制时间。仅凭“鼠标移动时卡顿”无法判断一定是 React 的问题。

记录可复现的条件:显示多少天、多少票型、是否打开侧栏、是否有请求,以及设备性能。优化前后使用同一组条件比较,不用一张偶然更快的截图证明效果。

区分临时预览和最终数据

拖拽经过的格子只是临时预览,不一定要每次都更新完整排期数组。即使把指针事件合并为每帧一次,只要每帧调用 setState,React 仍可能重新提交整张日历。一个更直接的做法是:

  1. 开始拖拽时只更新一次 React 状态,用它挂载预览矩形。
  2. 移动时把坐标写入 ref,用 requestAnimationFrame 合并同一帧内的多次事件。
  3. 在帧回调中只更新预览矩形的 transform、宽高和可见性,不触发日历网格的状态更新。
  4. 松开指针时根据起止位置计算业务选择,提交一次正式状态;取消或卸载时清理帧和全局监听器。

这里直接修改 DOM 只限于完全由拖拽逻辑拥有的临时覆盖层,不能让 React 与手工代码同时管理同一组样式。浏览器测试可以用 Profiler 包住日历网格:连续发送指针移动事件,确认矩形在移动,同时网格没有额外提交。这样测到的是优化目标,而不只是“最终选中结果正确”。

避免无效渲染

  • 把稳定的排期数据和短暂的拖拽状态分开,避免每次指针移动都重新计算筛选、排序和价格汇总。
  • 对实际昂贵且输入稳定的单元格使用 memo,并检查传入的对象、数组和回调是否每次都变。简单表达式不必为了“看起来优化”而包进 useMemo。
  • 日历很大时,只渲染可见范围或按周分段渲染;但虚拟化可能影响拖拽跨屏、键盘导航和焦点,需要单独验收。
  • 拖拽起点和终点都应由业务日期确定,不能只依赖像素坐标。滚动、缩放和响应式布局会改变坐标到日期的映射。

交互正确性同样重要

性能优化后检查:快速来回拖动、拖出容器、滚动时继续拖动、窗口失焦、触屏操作,以及组件卸载。预览可以丢帧,最终选择不能丢失;取消拖拽时也不能把临时范围写入正式排期。键盘和触屏入口需要有可用的替代交互。

最后再录制一次相同场景。若提交次数下降但主线程仍被布局或绘制占满,继续给 React 状态加缓存不会解决根因,应回到 DOM 规模和样式计算上排查。