Skip to content

Conversation

@cyfung1031
Copy link
Collaborator

@cyfung1031 cyfung1031 commented Feb 1, 2026

問題見 #1180 (comment)

hooks 和 index 的部份是 AI 重新合并再分开而成 (重构整体)
ScriptCard 和 ScriptTable 主要是人工修改+AI 评价 (重构 Draggable 相关)
主要针对 ScriptCard 修改。相关修改也放到 ScriptTable 了


问题概述(背景与动机)

在脚本列表页面(ScriptList)的实际使用中,存在较为明显的性能瓶颈,尤其在脚本数量较多(50+)的场景下,主要表现为:

  • 列表频繁发生闪烁和卡顿。例如在拖拽排序、启用/停用脚本、运行/停止脚本等操作时,整个列表(所有 ScriptCardItem 或表格行)会被重新渲染。
  • CPU 占用率较高,显著影响选项页的整体交互体验。
  • 该问题在 React 与 dnd-kit 组合使用时较为常见,但若不进行针对性优化,会直接导致页面流畅度下降,影响用户体验。

根因分析

经分析,问题主要由以下几方面共同导致:

  1. dnd-kit 的 Context 级联更新问题
    SortableContext 对 items 的变化高度敏感。当 scriptList 的数组引用发生变化(即使内容变化极小)时,SortableContext 会生成新的 items 数组,并触发所有使用 useSortable 的子组件重新计算位置。
    由于 Context 更新不受 React.memo 控制,最终导致整个列表被强制重渲染。

  2. 函数引用不稳定导致 memo 失效
    父组件向子组件传递的回调函数(如 handleRunStop 等)依赖于 t 或其他频繁变化的依赖项,导致每次渲染都会生成新的函数引用。
    在 props 对比阶段,React.memo 判定引用变化,从而触发不必要的组件重渲染。

  3. 状态更新缺乏精细化 diff 判断
    hooks 中的 updateScripts 等逻辑在更新状态时未区分“真实变化”和“无效更新”,即使仅修改单个脚本的局部字段(如 runStatus),也会创建新的列表对象并触发全量 setState 和 render。

  4. 其他影响 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,保证函数引用稳定。
  • 强化 ScriptCardItemReact.memo 比较逻辑,仅比较影响 UI 渲染的关键字段(如 namestatusrunStatus),忽略对视觉无影响的属性变化。
    - 列表 key 统一使用 item.uuid 作为稳定唯一标识,避免使用数组索引导致 DOM 重建。

效果
拖拽及常规操作时,仅相关节点发生更新;在移除 dnd-kit 的场景下,React.memo 可完全生效(当前仍保留拖拽能力)。

2. ScriptTable.tsx(表格视图优化)

改动原因
表格视图与卡片视图存在相同的问题:行组件依赖不稳定,在排序或操作单行时触发表格整体重渲染。

具体改动

  • 使用 useMemo 缓存排序结果和渲染数据,避免重复计算。
  • 所有事件处理函数(如 onClickonToggle)统一使用 useCallback,确保引用稳定。
  • 规范 dnd-kit 的使用方式:
    • items 仅传递 ID 数组
      ~ - 使用 rectSortingStrategy,减少布局计算开销~
  • 表格行 key 同样统一使用 uuid

效果
在大数据量表格中,对单行的操作不再触发整表重渲染。

3. hooks.tsx(状态管理与逻辑重构)

改动原因
原 hooks 实现缺乏变化判断和引用稳定机制,导致轻微状态更新被放大为全量更新,并沿组件树向下传播。

核心优化点

  • updateScripts
    新增精细化 diff 判断,仅在新旧值不一致时才更新状态,避免无意义的新对象创建和 setState
  • 消息监听逻辑(如 scriptRunStatus、enableScripts)
    在更新前先比较状态是否发生真实变化,防止无效 render。
  • 回调函数稳定化
    handleDeletehandleConfighandleRunStopscriptListSortOrder 等全部使用 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 对比优化前后的渲染树
  • 功能回归:
    • 拖拽排序结果持久化
    • 排序与服务端同步
    • 搜索防抖逻辑

@cyfung1031 cyfung1031 added P0 🚑 需要紧急处理的内容 hotfix 需要尽快更新到扩展商店 UI/UX 页面操作/显示相关 labels Feb 1, 2026
@CodFrm
Copy link
Member

CodFrm commented Feb 8, 2026

确实闪动,但是我记得我处理过一次,当时是Dnd的问题,不管了

Copy link
Contributor

Copilot AI left a 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 多语言文案

Comment on lines 76 to 82
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 };
Copy link

Copilot AI Feb 8, 2026

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) 更新判断。

Suggested change
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 };

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

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)}
Copy link

Copilot AI Feb 8, 2026

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)。

Suggested change
onClick={() => handleRunStop(item, t)}
onClick={() => handleRunStop(item)}

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

对, AI 改了后就不用 传 t 值
它用 useCallback 了

Copy link
Collaborator Author

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)。

Comment on lines 185 to 194
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);
}
});
Copy link

Copilot AI Feb 8, 2026

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。

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

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

Comment on lines 153 to 155
const next = arrayMove(prev, oldIdx, newIdx);
next.forEach((s, i) => (s.sort = i));
return next;
Copy link

Copilot AI Feb 8, 2026

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 作为派生数据而不是写回到已有对象上。

Suggested change
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 }));

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

其实在state 加个 .sort 不合适啦...
这个如果以 React 方式处理要全部 改 reference
我重新处理一下吧

Copy link
Collaborator Author

@cyfung1031 cyfung1031 Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

改这个太痛苦了
下回分解吧。这个PR先不理

sort 只影响整体排序而不排序单一元件
这样执行是没问题
但如果要找 AI agent 改代码的话肯定会改错

Comment on lines 184 to 185
selectedFilters: any,
searchRequest: SearchFilterRequest,
Copy link

Copilot AI Feb 8, 2026

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 或移除这些参数,并同步调整调用方。

Suggested change
selectedFilters: any,
searchRequest: SearchFilterRequest,
_selectedFilters: any,
_searchRequest: SearchFilterRequest,

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

先放著吧

Comment on lines 222 to 224
const tagItems = [
{ key: "all", label: t("script_list.sidebar.all"), icon: <IconTags />, count: Object.keys(tagMap).length },
...Object.keys(tagMap)
Copy link

Copilot AI Feb 8, 2026

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(或至少与其它分组保持一致的语义)。

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个 all 不是真的 all
国人写的插件,英文差没办法啦

Copy link
Collaborator Author

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 },
Copy link

Copilot AI Feb 8, 2026

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(或保持与其它分组一致的计数口径)。

Suggested change
{ 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 },

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个 all 不是真的 all
国人写的插件,英文差没办法啦

Copy link
Collaborator Author

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" 的话就会死掉

@cyfung1031 cyfung1031 marked this pull request as draft February 8, 2026 02:39
@cyfung1031
Copy link
Collaborator Author

cyfung1031 commented Feb 8, 2026

Copilot 指出的问题我顺便处理一下吧
大多都是之前的写法不太理想

处理了
.sort 这个先不搞

@cyfung1031 cyfung1031 marked this pull request as ready for review February 8, 2026 03:03
@cyfung1031 cyfung1031 marked this pull request as draft February 8, 2026 04:11
@cyfung1031
Copy link
Collaborator Author

4080014

fix TypeScript, change "all" to null, reconstruct sortScript

fix TypeScript

  • AI重构时都把类写成 any, 修正类

change "all" to null

  • 你写 "all" 的话,如果 tag 也是 "all" 就会出错。现在 null 代表 没filter

reconstruct sortScript

  • 对 ScriptCard 来说,用原本的插入不合理。例如第 14 个插到 第2个时,后面通通会移位。
  • 现在改成 Swap。把拖拉效果改动一下配合 ScriptCard 的显示
  • 把原本的 ArrayMove 拆成 ArrayMove 和 ArraySwap
  • 后台不要重复这个做法。直接经前台的 sort order 去做储存更新
  • 后台 sort 完后会返回结果给前台,让前台其他画面/Tab的次序更新
  • 把这个结果更新的 sorted 重构了。统一这个 sort 方法,有效避免实际资料和 sort uuids 不一致时的处理
  • 把 updatetime 改动拿掉了。这个 sort 改变只是显示问题,不是脚本实际有任何更新。脚本的名字代码数据完全不变
  • 改成 Promise.all 避免大量 await 影响 CPU 处理

@cyfung1031 cyfung1031 marked this pull request as ready for review February 8, 2026 06:50
@cyfung1031
Copy link
Collaborator Author

cyfung1031 commented Feb 8, 2026


columns 和 components 如果整个包住做Memo的话,其中一个参考改变就会直影响所有子物件,然后作为 props 传至 Table 元件

现在把他们都分割出来,每一个 Render function 各自根据自己的参数做参考更新
因此除了 t 改变,其他都不会直接影响 columns 和 components

( Table 仅在 scriptList 更新时 重绘相应部份 )


TitleCell 是 <div> 所以要做 React.Memo


另外发现 filterDropdown 这玩意会不断呼叫,里面的 setFilterKeys 和 confirm 可能没做稳定,一直改动
但 ScriptFilterNode 元件其实只会在 function triggering 才会用到 setFilterKeys 和 confirm
因此他们直接抽出 React绘图以外,在需要时取最新值即可

@cyfung1031
Copy link
Collaborator Author

cyfung1031 commented Feb 8, 2026

结合

@CodFrm 大问题都修好了

好像还有一些小问题,例如明明是打开侧栏的动作但它的文字显示是关闭。但不重要,功能都正常能用。

0a00fbf ee0c839 修好了 打开/关闭侧栏 的显示

screen-capture.25.online-video-cutter.com.-00.00.00.000-00.00.28.559.mp4
screen-capture.25.online-video-cutter.com.-00.00.39.934-00.01.51.848.mp4
screen-capture.25.online-video-cutter.com.-00.02.01.963-00.04.11.790.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hotfix 需要尽快更新到扩展商店 P0 🚑 需要紧急处理的内容 UI/UX 页面操作/显示相关

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants