-
Notifications
You must be signed in to change notification settings - Fork 314
[v1.3] 修正React重绘问题 (ScriptCard & ScriptTable) #1182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release/v1.3
Are you sure you want to change the base?
Conversation
…or just passing the props
|
确实闪动,但是我记得我处理过一次,当时是Dnd的问题,不管了 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
该 PR 主要围绕选项页脚本列表(ScriptList)在大量脚本场景下的卡顿/闪烁问题,重构了列表的数据管理与拖拽(dnd-kit)集成方式,通过稳定引用与更细粒度的状态更新来减少不必要的 React 重绘。
Changes:
- 重构 ScriptList 数据与过滤逻辑:拆分 hooks、引入更细的 diff 更新,减少无意义 setState。
- 重构 ScriptCard / ScriptTable 的拖拽相关实现:items 传递纯 uuid 列表,尽量稳定 context/handler 引用。
- 补充通用类型与 i18n 文案(operation_failed),并修复 favicon 加载相关的类型声明。
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/types/main.d.ts | 新增 ReactStateSetter 全局类型别名,简化 state setter 的 props 类型声明 |
| src/pages/store/favicons.ts | 为 favicon 批处理栈补充类型,减少 any,并加强 shift() 非空断言 |
| src/pages/options/routes/ScriptList/index.tsx | 重构 ScriptList 逻辑:引入 MainContent memo、内联 updateScripts/业务操作/过滤逻辑与拖拽排序 |
| src/pages/options/routes/ScriptList/hooks.tsx | 拆分为 useScriptDataManagement + useScriptFilters,并重写消息监听与统计构建逻辑 |
| src/pages/options/routes/ScriptList/Sidebar.tsx | 适配新的 TSelectFilter / ReactStateSetter 类型 |
| src/pages/options/routes/ScriptList/ScriptTable.tsx | 重构表格拖拽与 memo 策略,减少 dnd-kit context 级联更新影响 |
| src/pages/options/routes/ScriptList/ScriptCard.tsx | 重构卡片拖拽实现(DragHandle/DraggableEntry)并对整体组件做 memo 优化 |
| src/locales/*/translation.json | 增加 operation_failed 多语言文案 |
| setScriptList((prev) => { | ||
| const altered = new Set<string>(); | ||
| const newList = prev.map((s) => { | ||
| const item = chunkResults.find((r) => r.uuid === s.uuid); | ||
| if (item && s.favorite !== item.fav) { | ||
| altered.add(s.uuid); | ||
| return { ...s, favorite: item.fav }; |
Copilot
AI
Feb 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里对 prev 中每个脚本都做一次 chunkResults.find,会导致每个 favicon chunk 更新时变成 O(prev.length * chunkResults.length) 的查找;脚本数量较多时会明显放大 CPU 占用(与本 PR 优化目标相悖)。建议先把 chunkResults 预处理成 Map(uuid -> fav) 再单次遍历 prev 做 O(1) 更新判断。
| setScriptList((prev) => { | |
| const altered = new Set<string>(); | |
| const newList = prev.map((s) => { | |
| const item = chunkResults.find((r) => r.uuid === s.uuid); | |
| if (item && s.favorite !== item.fav) { | |
| altered.add(s.uuid); | |
| return { ...s, favorite: item.fav }; | |
| // 将 chunkResults 预处理为 Map,避免在列表遍历过程中重复执行 find | |
| const favMap = new Map(chunkResults.map((r) => [r.uuid, r.fav])); | |
| setScriptList((prev) => { | |
| const altered = new Set<string>(); | |
| const newList = prev.map((s) => { | |
| const fav = favMap.get(s.uuid); | |
| if (fav !== undefined && s.favorite !== fav) { | |
| altered.add(s.uuid); | |
| return { ...s, favorite: fav }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved in e703f6c
不错。日后再看看其他 .find 处理有没有这个问题
| icon={item.runStatus === SCRIPT_RUN_STATUS_RUNNING ? <RiStopFill /> : <RiPlayFill />} | ||
| loading={item.actionLoading} | ||
| size="mini" | ||
| onClick={() => handleRunStop(item, t)} |
Copilot
AI
Feb 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handleRunStop 在 ScriptCardProps 中的类型是 (item) => Promise,但 ScriptCardItemProps 仍声明为 (item, t) => Promise,并在这里传入 t。额外参数虽然运行时会被忽略,但类型/调用约定不一致会增加维护成本。建议统一签名:去掉 ScriptCardItemProps 的 t 参数,并把这里改为 handleRunStop(item)。
| onClick={() => handleRunStop(item, t)} | |
| onClick={() => handleRunStop(item)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
对, AI 改了后就不用 传 t 值
它用 useCallback 了
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved in 579a37b
统一签名:去掉 ScriptCardItemProps 的 t 参数,并把这里改为 handleRunStop(item)。
| let searchOrFilter = 0; | ||
| if (searchRequest.keyword) { | ||
| searchOrFilter = 1; | ||
| SearchFilter.requestFilterResult(searchRequest).then(() => { | ||
| if (searchOrFilter === 1) { | ||
| setFilterScriptList(list.filter((s) => SearchFilter.checkByUUID(s.uuid))); | ||
| } else { | ||
| setFilterScriptList(list); | ||
| } | ||
| }); |
Copilot
AI
Feb 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里的“取消/防竞态”逻辑有问题:effect cleanup 把 searchOrFilter 置 0 后,Promise resolve 仍会进入 else 分支并调用 setFilterScriptList(list),从而用旧的 list 覆盖新筛选结果(典型的 stale update)。建议改成 active 标志/AbortController:cleanup 时将 active=false,并在 then 内仅在 active 时更新;取消时不要再写回 list。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved in 421ebda
searchOrFilter -> enableKeywordSearch
| const next = arrayMove(prev, oldIdx, newIdx); | ||
| next.forEach((s, i) => (s.sort = i)); | ||
| return next; |
Copilot
AI
Feb 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里直接对 state 中的脚本对象做原地赋值(s.sort = i)。在当前 PR 通过 React.memo/引用稳定来减少重绘的前提下,这种 mutation 很容易导致依赖对象引用的 memo/比较逻辑失效或出现难以追踪的 UI 不更新问题。建议在更新 sort 时返回新对象(仅对 sort 变更的条目拷贝),或将 sort 作为派生数据而不是写回到已有对象上。
| const next = arrayMove(prev, oldIdx, newIdx); | |
| next.forEach((s, i) => (s.sort = i)); | |
| return next; | |
| const moved = arrayMove(prev, oldIdx, newIdx); | |
| return moved.map((s, i) => (s.sort === i ? s : { ...s, sort: i })); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
其实在state 加个 .sort 不合适啦...
这个如果以 React 方式处理要全部 改 reference
我重新处理一下吧
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
改这个太痛苦了
下回分解吧。这个PR先不理
sort 只影响整体排序而不排序单一元件
这样执行是没问题
但如果要找 AI agent 改代码的话肯定会改错
| selectedFilters: any, | ||
| searchRequest: SearchFilterRequest, |
Copilot
AI
Feb 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useScriptFilters 的参数 selectedFilters、searchRequest 当前在函数体内未使用,会触发 @typescript-eslint/no-unused-vars(该仓库该规则为 error)。如果暂时不用,建议改名为 _selectedFilters / _searchRequest 或移除这些参数,并同步调整调用方。
| selectedFilters: any, | |
| searchRequest: SearchFilterRequest, | |
| _selectedFilters: any, | |
| _searchRequest: SearchFilterRequest, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
先放著吧
| const tagItems = [ | ||
| { key: "all", label: t("script_list.sidebar.all"), icon: <IconTags />, count: Object.keys(tagMap).length }, | ||
| ...Object.keys(tagMap) |
Copilot
AI
Feb 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tagItems 的“all”项 count 目前使用 Object.keys(tagMap).length(标签数量),这会在侧边栏 UI 上把“全部脚本”的数量显示成“标签数”,与 status/type 的 all=scriptList.length 也不一致。建议这里的 count 使用 scriptList.length(或至少与其它分组保持一致的语义)。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个 all 不是真的 all
国人写的插件,英文差没办法啦
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
呀。我现在才理解 CodFrm 把 "all" 跟 string number 混在一起。那这样不行了
如果 tag 是 "all" 的话就会死掉
| })), | ||
| ]; | ||
| const sourceItems = [ | ||
| { key: "all", label: t("script_list.sidebar.all"), icon: <IconLink />, count: Object.keys(originMap).length }, |
Copilot
AI
Feb 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sourceItems 的“all”项 count 目前使用 Object.keys(originMap).length(来源域名数量),会导致“全部脚本”的数量显示不正确/语义不一致。建议改为 scriptList.length(或保持与其它分组一致的计数口径)。
| { key: "all", label: t("script_list.sidebar.all"), icon: <IconLink />, count: Object.keys(originMap).length }, | |
| { key: "all", label: t("script_list.sidebar.all"), icon: <IconLink />, count: scriptList.length }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个 all 不是真的 all
国人写的插件,英文差没办法啦
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
呀。我现在才理解 CodFrm 把 "all" 跟 string number 混在一起。那这样不行了
如果 tag 是 "all" 的话就会死掉
|
处理了 |
fix TypeScript, change "all" to null, reconstruct sortScriptfix TypeScript
change "all" to null
reconstruct sortScript
|
|
columns 和 components 如果整个包住做Memo的话,其中一个参考改变就会直影响所有子物件,然后作为 props 传至 Table 元件 现在把他们都分割出来,每一个 Render function 各自根据自己的参数做参考更新 ( Table 仅在 scriptList 更新时 重绘相应部份 ) TitleCell 是 另外发现 filterDropdown 这玩意会不断呼叫,里面的 setFilterKeys 和 confirm 可能没做稳定,一直改动 |
|
结合
@CodFrm 大问题都修好了 好像还有一些小问题, 0a00fbf ee0c839 修好了 打开/关闭侧栏 的显示 screen-capture.25.online-video-cutter.com.-00.00.00.000-00.00.28.559.mp4screen-capture.25.online-video-cutter.com.-00.00.39.934-00.01.51.848.mp4screen-capture.25.online-video-cutter.com.-00.02.01.963-00.04.11.790.mp4 |
問題見 #1180 (comment)
hooks 和 index 的部份是 AI 重新合并再分开而成 (重构整体)
ScriptCard 和 ScriptTable 主要是人工修改+AI 评价 (重构 Draggable 相关)
主要针对 ScriptCard 修改。相关修改也放到 ScriptTable 了
问题概述(背景与动机)
在脚本列表页面(ScriptList)的实际使用中,存在较为明显的性能瓶颈,尤其在脚本数量较多(50+)的场景下,主要表现为:
根因分析
经分析,问题主要由以下几方面共同导致:
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 效率的细节问题
上述问题叠加,最终导致列表在高数据量场景下出现明显的性能劣化。
解决方案与关键改动说明
本次优化围绕 “稳定引用、减少 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,确保引用稳定。items仅传递 ID 数组~ - 使用
rectSortingStrategy,减少布局计算开销~uuid。效果
在大数据量表格中,对单行的操作不再触发整表重渲染。
3. hooks.tsx(状态管理与逻辑重构)
改动原因
原 hooks 实现缺乏变化判断和引用稳定机制,导致轻微状态更新被放大为全量更新,并沿组件树向下传播。
核心优化点
新增精细化 diff 判断,仅在新旧值不一致时才更新状态,避免无意义的新对象创建和
setState。在更新前先比较状态是否发生真实变化,防止无效 render。
handleDelete、handleConfig、handleRunStop、scriptListSortOrder等全部使用useCallback,并移除不必要的依赖(如稳定的t)。filterFuncs、统计数据等通过useMemo精准依赖,减少重复计算map + filter(Boolean)重建排序列表,逻辑更清晰all项使用真实数量而非脚本总数整体收益
hooks 逻辑更加模块化、可读性更强,类型安全性提升,同时显著减少无效渲染(80%+)。
4. 其他非核心优化
types/main.d.ts:细化类型定义,增强 TypeScript 严格性。性能收益与测试建议
性能收益
建议测试项
可通过
console.log("Rendered")验证是否存在全量重渲染