高频手势缩放不要走 React 状态

**触控板捏合 / 滚轮缩放掉帧,根因通常不是「事件太多」,而是把每次事件都塞进了 React state,还顺带改了大 DOM 的布局。** 正确做法:缩放过程中用 `ref + requestAnimationFrame` 直接改预览 DOM 的 `style.zoom` 和外壳宽度,React state 只保存最终倍率 / 指示器 / 重置。

一、掉帧的五个叠加原因

  1. 每次捏合都 setUserZoom —— pinch 会连续触发大量 wheel 事件,每个事件都进 React state,触发整棵预览重新 render。
  2. CSS zoom 不是 GPU 合成层动画 —— zoom 会触发布局重算与重绘;transform: scale() 多数情况可以走 compositor。
  3. 缩放同时改外壳宽度 —— zoomedWidth = A4_WIDTH_PX * effectiveZoom 每次变化都会让外层滚动区的 scrollWidth 重算。
  4. 预览 DOM 被重复渲染 —— 隐藏测量容器 + 可见分页,缩放 state 变化会把它们一起走一遍。
  5. 没有 requestAnimationFrame 节流 —— wheel 频率可能高于屏幕刷新率,事件来一次更新一次,主线程被打满。

真正的组合是:CSS zoom + React state 高频更新 + 大 DOM 重排。

二、可迁移的判断

  • 高频连续手势(pinch / drag / resize)不该驱动框架状态,应该直接写 DOM 样式;框架状态只留「最终值」。
  • 优化方向不是换掉 zoom,而是把高频路径移出渲染链路——保留 zoom 的纵向滚动优势(transform: scale 不改变布局尺寸,滚动条行为会变)。
  • 定位这类问题时,先问「这个值变一次,要重排多大的 DOM」,而不是先怀疑事件监听写错了。

三、跨平台适配:判断修饰键语义,不判断系统

缩放逻辑里写 if (!e.ctrlKey && !e.metaKey) return;,就同时覆盖了 macOS(Cmd + 滚轮)和 Windows(Ctrl + 滚轮、Precision Touchpad 捏合在 Chrome/Edge 里通常也表现为 ctrlKey + wheel)。

不要按操作系统分支——按输入语义(修饰键)判断,能力自然跨平台。没适配的部分要明说:触屏 pinch 需要额外的 pointer/touch 手势逻辑,当前没有。

相关

  • flex子项的百分比max-height不可靠 —— 同类:CSS 机制本身决定了代价
  • 进度条要由真实进度驱动 —— 高频 UI 更新的另一种状态设计