[v1.3] 修正React重绘问题 (Popup)#1181
Conversation
There was a problem hiding this comment.
Pull request overview
该 PR 旨在缓解 Popup 页面在脚本数量较多时的 React 额外重绘/卡顿问题,通过尽量复用 list/item 的引用与缓存异步 metadata 合并结果,减少不必要的组件更新。
Changes:
- 在
Popup/App.tsx抽出updateList,在子项无变化时复用原 list 引用,降低setState触发的重绘频率 - 在
ScriptMenuList中引入 metadata 合并缓存(Map+WeakMap),尽量复用合并后的 item 引用,减少列表项的重复渲染 - 调整
ScriptSetting的useMemo依赖,避免仅因script对象引用变化导致配置项重算
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/pages/popup/App.tsx | 新增 updateList,尝试在启用/删除/菜单更新时减少不必要的 list 引用变化与重绘 |
| src/pages/components/ScriptSetting/index.tsx | useMemo 依赖从 script 调整为 script.uuid,降低无关重算 |
| src/pages/components/ScriptMenuList/index.tsx | 重构 metadata 异步合并逻辑,引入模块级缓存与 WeakMap 复用 merged item 引用 |
| if (!changed && list.map((e) => e.uuid).join(",") !== newList.map((e) => e.uuid).join(",")) { | ||
| // 单一项未有改变,但因为 sort值改变 而改变了次序 | ||
| changed = true; |
There was a problem hiding this comment.
list.map(...).join(",") !== newList.map(...).join(",") 这段在 !changed 时每次都会做两次 O(n) 的遍历/拼接,即使没有开启 sort 也会执行,可能抵消这里想减少重绘的优化效果。建议仅在 options?.sort 为 true(且确实需要判断排序变化)时再做该比较,或用更轻量的顺序比较方式。
| if (!changed && list.map((e) => e.uuid).join(",") !== newList.map((e) => e.uuid).join(",")) { | |
| // 单一项未有改变,但因为 sort值改变 而改变了次序 | |
| changed = true; | |
| if (!changed && options?.sort) { | |
| // 单一项未有改变,但因为 sort值改变 而改变了次序 | |
| if (list.length !== newList.length) { | |
| changed = true; | |
| } else { | |
| for (let i = 0; i < list.length; i++) { | |
| if (list[i].uuid !== newList[i].uuid) { | |
| changed = true; | |
| break; | |
| } | |
| } | |
| } |
There was a problem hiding this comment.
太长了。这个 micro optimization 不值得
另外这个对比在 前跟后的sort值不一致 的情况都会起作用
| metadata = script?.metadata || {}; // 即使 scriptDAO 返回失败也 fallback 一个空物件 | ||
| cacheMetadata.set(uuid, metadata); |
There was a problem hiding this comment.
fetchMergedList 在 scriptDAO.get(uuid) 读不到脚本时会把 metadata 回退成 {} 并写入 cacheMetadata,导致后续即使数据恢复也不会再尝试读取真实 metadata(缓存被“永久”填充)。建议只在成功读取到 script 时才写入缓存,或缓存 undefined 以允许后续重试/失效刷新。
| metadata = script?.metadata || {}; // 即使 scriptDAO 返回失败也 fallback 一个空物件 | |
| cacheMetadata.set(uuid, metadata); | |
| if (script) { | |
| // 只有在成功读取到脚本时才写入缓存 | |
| metadata = script.metadata || {}; | |
| cacheMetadata.set(uuid, metadata); | |
| } else { | |
| // 读取失败时只在当前调用中使用空物件,不写入缓存,允许后续重试 | |
| metadata = {}; | |
| } |
There was a problem hiding this comment.
对。如果真的发生读取失败,就不要再尝试
不然这个物件参考会改不停
| const updateList = (list: ScriptMenu[], update: TUpdateEntryFn, options: TUpdateListOption | undefined) => { | ||
| // 如果更新跟当前 list 的子项无关,则不用更改 list 的物件参考 | ||
| const newList = []; | ||
| let changed = false; | ||
| for (let i = 0; i < list.length; i++) { | ||
| const oldItem = list[i]; | ||
| const newItem = update(oldItem); // 如没有更改,物件参考会保持一致 | ||
| if (newItem !== oldItem) changed = true; | ||
| if (newItem) { | ||
| newList.push(newItem); | ||
| } | ||
| } | ||
| if (options?.sort) { | ||
| newList.sort(scriptListSorter); | ||
| } | ||
| if (!changed && list.map((e) => e.uuid).join(",") !== newList.map((e) => e.uuid).join(",")) { | ||
| // 单一项未有改变,但因为 sort值改变 而改变了次序 | ||
| changed = true; | ||
| } | ||
| return changed ? newList : list; // 如子项没任何变化,则返回原list参考 | ||
| }; |
There was a problem hiding this comment.
这里新增了 updateList(决定是否复用旧 list 引用、以及排序/删除逻辑),属于影响渲染与状态更新的关键路径;当前 tests/pages/popup/App.test.tsx 只覆盖了基础渲染,未覆盖“无变更时必须保持引用不变 / sort 时顺序变化需触发更新”等场景。建议补充针对该更新策略的测试(可考虑抽到独立 util 后做纯函数单测),避免后续优化回归。
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
垃圾React设计(State设计)导致额外的心力去解决那些烦人的UI重绘问题
#1180 (comment)
你只有20个脚本的话重绘问题是看不出来
但我有差不多100个的话,按钮一按什么的很容易就看出来:「肯定又是垃圾React的问题」