-
Notifications
You must be signed in to change notification settings - Fork 166
perf: skipping re-rendering #190
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: master
Are you sure you want to change the base?
perf: skipping re-rendering #190
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub.
|
冲突了 |
c734dd8
to
83d9a75
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #190 +/- ##
==========================================
- Coverage 97.85% 97.49% -0.37%
==========================================
Files 19 19
Lines 794 797 +3
Branches 193 189 -4
==========================================
Hits 777 777
- Misses 17 20 +3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
@afc163 大佬,为啥这个还没有合并呢?都几年了,这个问题很严重啊 |
@muzea @XianZhengquan 冲突了 |
@muzea 大佬,瞅一瞅呀 |
b505549
to
6b1ebb8
Compare
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. Walkthrough在多个组件与钩子中引入 useMemo/useCallback 以稳定函数与对象引用,并将测试中多处 DOM 交互与定时器推进包装到 React 的 act() 中;所有对外行为与公共 API 均未改变,变更为内部优化与测试修正(<=50 字)。 Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User as User
participant Filler as Filler
participant RO as ResizeObserver
participant Parent as Parent(onInnerResize)
User->>Filler: Render
Filler->>RO: Observe inner
RO-->>Filler: resize(entry.offsetHeight)
alt offsetHeight > 0
Filler->>Filler: handleResize (useCallback)
Filler->>Parent: onInnerResize(offsetHeight)
else offsetHeight == 0
Filler-->>Parent: (no call)
end
sequenceDiagram
autonumber
participant List as List
participant Hooks as useChildren
participant Render as renderFunc
participant Item as <Item>
List->>Hooks: 提供 list, startIndex..endIndex, getKey, offsetX, scrollWidth
Hooks->>Hooks: useMemo(children[]) [依赖稳定?]
alt 依赖未变
Hooks-->>List: 返回缓存 children[]
else 依赖变化
loop i in [startIndex, endIndex)
Hooks->>Render: renderFunc({ index, width, offsetX })
Render-->>Hooks: element
Hooks->>Item: 包装并设 key = getKey(...)
Item-->>Hooks: itemElement
end
Hooks-->>List: 返回新 children[]
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
最近事情比较多,我看看这几天 rebase 一下代码 |
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
This PR optimizes performance by preventing unnecessary re-renders through the addition of React memoization techniques. The changes wrap functions and objects in useCallback
and useMemo
hooks to maintain reference stability across renders.
- Wraps functions in
useCallback
to prevent recreation on every render - Adds
useMemo
to objects and computed values to maintain reference equality - Includes a Chinese comment explaining potential behavioral differences
Reviewed Changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
File | Description |
---|---|
src/hooks/useHeights.tsx | Converts functions to useCallback and adds explanatory comment |
src/hooks/useChildren.tsx | Wraps return value in useMemo with dependency array |
src/List.tsx | Memoizes sharedConfig object creation |
src/Filler.tsx | Reorders imports and wraps resize handler in useCallback |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
|
||
useEffect(() => { | ||
return cancelRaf; | ||
}, []); | ||
|
||
// 这里稍显迷惑性,当 heightsRef.current.set 被调用时,updatedMark 会变化,进而导致 heightsRef.current 也出现变化 |
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.
The comment is in Chinese. Consider translating to English for consistency with the rest of the codebase: // This is somewhat confusing: when heightsRef.current.set is called, updatedMark changes, which causes heightsRef.current to also change
// 这里稍显迷惑性,当 heightsRef.current.set 被调用时,updatedMark 会变化,进而导致 heightsRef.current 也出现变化 | |
// This is somewhat confusing: when heightsRef.current.set is called, updatedMark changes, which causes heightsRef.current to also change |
Copilot uses AI. Check for mistakes.
}, | ||
offsetX, | ||
}) as React.ReactElement; | ||
// 可能存在 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.
The comment is in Chinese. Consider translating to English for consistency: // There may be cases where the list doesn't change but the data inside it does, which will behave differently from the previous implementation
// 可能存在 list 不变但是里面的数据存在变化的情况,会与之前写法存在不同的行为 | |
// There may be cases where the list reference does not change, but the data inside it does, which will behave differently from the previous implementation |
Copilot uses AI. Check for mistakes.
@@ -88,11 +88,12 @@ export default function useHeights<T>( | |||
onItemRemove?.(item); | |||
} | |||
} | |||
} | |||
}, [collectHeight, getKey, onItemAdd, onItemRemove]); | |||
|
|||
useEffect(() => { | |||
return cancelRaf; | |||
}, []); |
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.
The useEffect cleanup function should include cancelRaf
in the dependency array since it's now a memoized callback. Change the dependency array from []
to [cancelRaf]
.
}, []); | |
}, [cancelRaf]); |
Copilot uses AI. Check for mistakes.
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.
Actionable comments posted: 0
🧹 Nitpick comments (6)
src/Filler.tsx (1)
72-79
: 为 handleResize 标注类型,避免隐式 any;保持回调稳定性当前解构参数未标注类型,在开启 noImplicitAny 时会报错。建议使用 rc-resize-observer 的类型来声明,既避免隐式 any,又与 onResize 的签名一致。
应用如下修改:
-import ResizeObserver from 'rc-resize-observer'; +import ResizeObserver from 'rc-resize-observer'; +import type { ResizeObserverProps } from 'rc-resize-observer';- const handleResize = React.useCallback( - ({ offsetHeight }) => { + const handleResize: ResizeObserverProps['onResize'] = React.useCallback( + ({ offsetHeight }) => { if (offsetHeight && onInnerResize) { onInnerResize(); } }, [onInnerResize], );Also applies to: 84-85
src/hooks/useChildren.tsx (2)
15-16
: 确认“list 引用不变但内部数据变化”的行为差异是否可接受这里明确引入了记忆化,若外部在原地 mutate 数组(保持引用不变),则不会重新计算子元素,可能导致 UI 不更新。请确认库的契约是否要求数据不可变更新(推荐做法)。若需兼容原地变更,建议增加一个外部可控的“数据版本号”或“无效化”信号参与依赖。
如需,我可以起草一个 dataVersion(或 invalidate)的 prop 方案,并补充到 List 与 useChildren 的依赖中。
16-33
: 记忆化计算本身 LGTM,但可补充 list.length 捕获原地增删场景当前依赖包含 list 本身,但无法捕获“同引用但长度变化”的极端情况。增加 list.length 作为低成本的折中方案,可覆盖 push/pop 等原地操作(仍无法捕获仅内容变更但长度不变的情况)。
- }, [list, startIndex, endIndex, setNodeRef, renderFunc, getKey, offsetX, scrollWidth]); + }, [list, list.length, startIndex, endIndex, setNodeRef, renderFunc, getKey, offsetX, scrollWidth]);src/hooks/useHeights.tsx (3)
31-71
: 微调:避免重复自增 promiseIdRefcollectHeight 一开始已调用 cancelRaf() 自增了 promiseIdRef;异步分支中再次自增会造成语义上的冗余(功能上无害)。建议删除第二次自增,提升可读性。
- promiseIdRef.current += 1; const id = promiseIdRef.current; Promise.resolve().then(() => { if (id === promiseIdRef.current) { doCollect(); } });
72-92
: setInstanceRef 记忆化良好;布尔差异判断可读性可提升当前用法
!origin !== !instance
属于“真假异或”,可读性略差。建议改为显式布尔转换对比。- // Instance changed - if (!origin !== !instance) { + // Instance changed + if (Boolean(origin) !== Boolean(instance)) { if (instance) { onItemAdd?.(item); } else { onItemRemove?.(item); } }
93-96
: useEffect 依赖建议包含 cancelRaf 以满足 exhaustive-deps虽然 cancelRaf 已稳定,但加入依赖可避免 ESLint 提示。
- useEffect(() => { - return cancelRaf; - }, []); + useEffect(() => { + return cancelRaf; + }, [cancelRaf]);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
src/Filler.tsx
(2 hunks)src/List.tsx
(1 hunks)src/hooks/useChildren.tsx
(1 hunks)src/hooks/useHeights.tsx
(3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/List.tsx (1)
src/interface.ts (1)
SharedConfig
(7-9)
src/hooks/useChildren.tsx (1)
src/Item.tsx (1)
Item
(8-16)
🔇 Additional comments (4)
src/Filler.tsx (1)
2-3
: 引入 rc-resize-observer 没问题依赖与用法与全局保持一致。
src/hooks/useHeights.tsx (2)
27-30
: 将 cancelRaf 记忆化以稳定引用:LGTM命名 + 空依赖的 useCallback 简洁明了,有助于下游依赖稳定。
97-99
: 返回 heightsRef.current 的模式保持不变:LGTM通过 updatedMark 驱动外部重新计算,高度缓存实例保持稳定引用,符合现有用法。
src/List.tsx (1)
168-173
: sharedConfig 使用 useMemo 稳定引用:LGTM以 getKey 为依赖可避免每次 render 产生新对象,降低子树无谓重渲染,符合本 PR 的目标。
return { | ||
getKey, | ||
}; | ||
}, [getKey]); |
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.
这个 sharedConfig 里只有一个 getKey,不如把参数直接拍平,不要这层 sharedConfig 了。
@zombieJ 也看看 |
感谢大佬 🙏 |
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.
Actionable comments posted: 0
🧹 Nitpick comments (2)
tests/scrollWidth.test.tsx (1)
233-235
: 如后续 scrollTo 存在异步链路,可考虑切换为 await act当前实现是同步断言,没问题;若未来 scrollTo 内部通过计时器/raf 异步更新,建议切换为 await act(async () => …) 并在内部推进计时器,减少潜在偶现。
Also applies to: 238-240
tests/scroll.test.js (1)
98-103
: 建议调整 act 中的语句顺序:先触发 scrollTo 再推进计时器当前先 runAllTimers 再 scrollTo,若 scrollTo 内部存在异步(如 raf/setTimeout),会遗漏本次 scrollTo 产生的任务,存在偶发不稳定风险。建议交换顺序:
@@ - act(() => { - jest.runAllTimers(); - - listRef.current.scrollTo(null); - }); + act(() => { + listRef.current.scrollTo(null); + jest.runAllTimers(); + }); @@ - act(() => { - jest.runAllTimers(); - - listRef.current.scrollTo(null); - }); + act(() => { + listRef.current.scrollTo(null); + jest.runAllTimers(); + });Also applies to: 400-405
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
tests/scroll-Firefox.test.js
(2 hunks)tests/scroll.test.js
(5 hunks)tests/scrollWidth.test.tsx
(1 hunks)tests/touch.test.js
(3 hunks)
🔇 Additional comments (10)
tests/scrollWidth.test.tsx (1)
233-235
: 将 ref.scrollTo 调用包裹到 act 中,行为更稳健把同步的 scrollTo 调用放入 act,符合 React 对受控更新的测试约定,避免 “not wrapped in act(...)” 的告警。断言读取 getScrollInfo 也是同步的,改动合理。
Also applies to: 238-240
tests/scroll-Firefox.test.js (2)
1-1
: 统一使用 @testing-library/react 的 act,方向正确改为从 RTL 引入 act 与本仓库其余测试保持一致,避免跨包 act 实例不一致的问题。
127-130
: 滚动到底部与计时器推进放进 act,避免状态未刷新断言将 scrollTo 与 jest.runAllTimers 放到同一个 act 中,有助于确保副作用在断言前已生效。改动到位。
tests/scroll.test.js (1)
114-117
: 将 scrollTo 与计时器统一包裹到 act 中,符合测试最佳实践这些位置的顺序与时机处理合理:先触发,再推进计时器,保证状态在断言前被刷新。改动 +1。
Also applies to: 135-137, 161-164
tests/touch.test.js (6)
74-91
: 触摸序列整体包裹进单个 act,降低异步时序带来的不确定性start/move/end 与计时器推进放在同一 act 中,能确保副作用在断言前完成。实现合理。
106-117
: “不可滚动时调用 preventDefault” 场景的事件派发包裹在 act 中,合理在 act 中构造与派发 touch 事件并注入 preventDefault mock,能稳定覆盖逻辑分支。
121-137
: 重新起一轮触摸交互并在 act 内重置 mock,保证隔离性在同一 act 中推进计时器、reset mock 并再次派发事件,保证前后两段交互不串扰。用法到位。
146-150
: 容器 touchstart 包裹 act,确保同步副作用按期落地轻量但必要的包裹,避免 React 对未包裹更新的告警。
155-170
: 嵌套用例迁移到 RTL 并显式指定 itemKey,稳定性更好使用 RTL 的 render + container 模式,并为外层 List 指定 itemKey,可避免 key 生成差异导致的不必要重渲染。合理的微调。
182-185
: 在异步 act 中推进计时器,确保嵌套滚动副作用完成advanceTimersByTime 放在 await act(async () => …) 中,能保证最终断言观察到稳定状态。改动得当。
ant-design/ant-design#34182
Summary by CodeRabbit
重构
测试
文档