React中医学图像Canvas绘制的撤销/重做功能优化
针对你当前遇到的全量保存Uint8ClampedArray导致mouseup卡顿的问题,可以先尝试以下几个简便的优化方案,不用直接切换到复杂的增量历史记录:
1. 替换低效的数组复制方式
你当前用Uint8ClampedArray.from(currentArray)复制数组,这个方法会先将TypedArray转换为普通数组再重新创建,性能远低于直接使用TypedArray的构造函数复制。
修改handleMouseUp中的代码:
function handleMouseUp() { if (!currentArray) return; // 直接用构造函数复制,是TypedArray最快的复制方式 setState(new Uint8ClampedArray(currentArray)); }
2. 异步延迟执行历史保存
将历史状态的保存操作放到浏览器空闲时段执行,避免阻塞UI线程导致卡顿。可以用requestIdleCallback实现:
function handleMouseUp() { if (!currentArray) return; // 让浏览器在空闲时执行保存,超时1秒确保最终会完成 requestIdleCallback(() => { setState(new Uint8ClampedArray(currentArray)); }, { timeout: 1000 }); }
这样用户完成绘制松开鼠标时,不会立刻触发大数组复制,而是等待浏览器处理完UI渲染后再后台执行,完全不会感知到卡顿。
3. 优化历史记录的更新逻辑
原Hook中states.slice(startIndex, index + 1)会创建一个新数组,当历史记录较多时会产生额外开销。可以改用函数式更新+数组splice操作,减少不必要的数组创建:
修改useUndoableState中的setState方法:
const setState = (value: TypedArray) => { setStates(prevStates => { const updated = [...prevStates]; // 清除当前索引之后的所有重做状态 updated.splice(index + 1); // 添加新状态 updated.push(value); // 超过历史长度限制时移除最旧的状态 if (updated.length > historySize) { updated.shift(); // 移除首个元素后,当前索引同步减1 setIndex(prev => Math.max(0, prev - 1)); } else { setIndex(updated.length - 1); } return updated; }); };
这种方式避免了依赖外部的states和index值,同时减少了数组切片的开销。
4. 避免无意义的重复保存
如果用户只是点击画布但没有任何绘制操作,不需要保存重复状态。可以添加一个“脏标记”来判断是否需要保存:
const [isDirty, setIsDirty] = useState(false); function handleDrawing(){ // 原有绘制逻辑... const newArray = addCanvasStrokeToArrayState(imageData, slice); setCurrentArray(newArray); setIsDirty(true); // 标记数组已修改 } function handleMouseUp() { if (!currentArray || !isDirty) return; setIsDirty(false); // 重置脏标记 requestIdleCallback(() => { setState(new Uint8ClampedArray(currentArray)); }, { timeout: 1000 }); }
这样只有当实际发生绘制修改时才会触发历史保存,减少不必要的数组复制。
如果以上优化后性能仍然达不到要求,再考虑实现增量式历史记录:记录每次绘制的操作详情(比如画笔的位置、影响的体素范围、修改前后的值),撤销时反向还原这些修改,而不是保存整个数组。这种方式内存占用会大幅降低,但需要额外维护操作日志和反向还原逻辑,适合对性能要求极高的场景。
内容的提问来源于stack exchange,提问作者Herman Bjørnerud

