[v1.3] 修正React重绘问题 (ScriptCard & ScriptTable) by cyfung1031 · Pull Request #1182 · scriptscat/scriptcat
問題見 #1180 (comment)
hooks 和 index 的部份是 AI 重新合并再分开而成 (重构整体)
ScriptCard 和 ScriptTable 主要是人工修改+AI 评价 (重构 Draggable 相关)
主要针对 ScriptCard 修改。相关修改也放到 ScriptTable 了
问题概述(背景与动机)
在脚本列表页面(ScriptList)的实际使用中,存在较为明显的性能瓶颈,尤其在脚本数量较多(50+)的场景下,主要表现为:
- 列表频繁发生闪烁和卡顿。例如在拖拽排序、启用/停用脚本、运行/停止脚本等操作时,整个列表(所有 ScriptCardItem 或表格行)会被重新渲染。
- CPU 占用率较高,显著影响选项页的整体交互体验。
- 该问题在 React 与 dnd-kit 组合使用时较为常见,但若不进行针对性优化,会直接导致页面流畅度下降,影响用户体验。
根因分析
经分析,问题主要由以下几方面共同导致:
-
dnd-kit 的 Context 级联更新问题
SortableContext 对items的变化高度敏感。当scriptList的数组引用发生变化(即使内容变化极小)时,SortableContext 会生成新的items数组,并触发所有使用useSortable的子组件重新计算位置。
由于 Context 更新不受React.memo控制,最终导致整个列表被强制重渲染。 -
函数引用不稳定导致 memo 失效
父组件向子组件传递的回调函数(如handleRunStop等)依赖于t或其他频繁变化的依赖项,导致每次渲染都会生成新的函数引用。
在 props 对比阶段,React.memo判定引用变化,从而触发不必要的组件重渲染。 -
状态更新缺乏精细化 diff 判断
hooks 中的updateScripts等逻辑在更新状态时未区分“真实变化”和“无效更新”,即使仅修改单个脚本的局部字段(如runStatus),也会创建新的列表对象并触发全量setState和 render。 -
其他影响 diff 效率的细节问题
- 使用数组索引作为 key,导致 React 在列表变动时无法复用节点。
- useEffect / useMemo / useCallback 依赖控制不够严格,增加了不必要的重新计算。
上述问题叠加,最终导致列表在高数据量场景下出现明显的性能劣化。
解决方案与关键改动说明
本次优化围绕 “稳定引用、减少 Context 级联影响、提升 diff 精度” 三个核心方向,对相关组件和 hooks 进行了重构。所有改动均为性能优化,不涉及功能行为变更。
1. ScriptCard.tsx(卡片视图优化)
改动原因
旧实现中,sortableIds 每次渲染都会通过 map 生成对象数组,增加了 dnd-kit 内部对比成本,并放大了 Context 更新的影响范围。
具体改动
- 使用
useMemo缓存sortableIds,仅生成纯字符串 ID 数组(如['uuid1', 'uuid2']),避免传递对象引用。
dnd-kit 对字符串数组的比较效率更高,可显著减少无效更新。 - 使用
useCallback包裹handleDragEnd,并精简依赖项,仅依赖scriptListSortOrder,保证函数引用稳定。 - 强化
ScriptCardItem的React.memo比较逻辑,仅比较影响 UI 渲染的关键字段(如name、status、runStatus),忽略对视觉无影响的属性变化。
- 列表 key 统一使用item.uuid作为稳定唯一标识,避免使用数组索引导致 DOM 重建。
效果
拖拽及常规操作时,仅相关节点发生更新;在移除 dnd-kit 的场景下,React.memo 可完全生效(当前仍保留拖拽能力)。
2. ScriptTable.tsx(表格视图优化)
改动原因
表格视图与卡片视图存在相同的问题:行组件依赖不稳定,在排序或操作单行时触发表格整体重渲染。
具体改动
- 使用
useMemo缓存排序结果和渲染数据,避免重复计算。 - 所有事件处理函数(如
onClick、onToggle)统一使用useCallback,确保引用稳定。 - 规范 dnd-kit 的使用方式:
items仅传递 ID 数组
~ - 使用rectSortingStrategy,减少布局计算开销~
- 表格行 key 同样统一使用
uuid。
效果
在大数据量表格中,对单行的操作不再触发整表重渲染。
3. hooks.tsx(状态管理与逻辑重构)
改动原因
原 hooks 实现缺乏变化判断和引用稳定机制,导致轻微状态更新被放大为全量更新,并沿组件树向下传播。
核心优化点
- updateScripts
新增精细化 diff 判断,仅在新旧值不一致时才更新状态,避免无意义的新对象创建和setState。 - 消息监听逻辑(如 scriptRunStatus、enableScripts)
在更新前先比较状态是否发生真实变化,防止无效 render。 - 回调函数稳定化
handleDelete、handleConfig、handleRunStop、scriptListSortOrder等全部使用useCallback,并移除不必要的依赖(如稳定的t)。 - 搜索与过滤逻辑优化
- 使用 active 标志防止异步搜索竞态问题
filterFuncs、统计数据等通过useMemo精准依赖,减少重复计算
- 其他调整
- 使用
map + filter(Boolean)重建排序列表,逻辑更清晰 - 标签及来源统计修正,
all项使用真实数量而非脚本总数
- 使用
整体收益
hooks 逻辑更加模块化、可读性更强,类型安全性提升,同时显著减少无效渲染(80%+)。
4. 其他非核心优化
- Sidebar:微调渲染逻辑,减少父子组件间的更新传染。
types/main.d.ts:细化类型定义,增强 TypeScript 严格性。
性能收益与测试建议
性能收益
- 列表操作过程流畅,闪烁和卡顿问题消失
- 渲染次数下降
- 在大规模脚本场景下,CPU 占用显著降低
建议测试项
- 50+ 脚本场景下:拖拽排序、运行/停止、搜索操作
可通过console.log("Rendered")验证是否存在全量重渲染 - 使用 React DevTools Profiler 对比优化前后的渲染树
- 功能回归:
- 拖拽排序结果持久化
- 排序与服务端同步
- 搜索防抖逻辑