Skip to content

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

muzea
Copy link

@muzea muzea commented Nov 13, 2022

ant-design/ant-design#34182

Summary by CodeRabbit

  • 重构

    • 对多个组件与钩子中的回调与计算进行 memo 化/稳定化,减少不必要的重渲染与子元素重建,保持外部 API 与布局行为不变。
  • 测试

    • 多处测试用例改为在 React 的 act() 中执行异步与事件操作,改进测试稳定性;部分测试采用更合适的测试库方式并调整用例输入(不改变被测逻辑)。
  • 文档

    • 新增注释,提示当列表引用不变但内部数据改变时的潜在行为差异。

@vercel
Copy link

vercel bot commented Nov 13, 2022

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
virtual-list Ready Ready Preview Comment Aug 20, 2025 3:00pm

@afc163
Copy link
Member

afc163 commented Dec 27, 2022

冲突了

@codecov
Copy link

codecov bot commented Dec 31, 2022

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.49%. Comparing base (5769845) to head (c973f19).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@muzea
Copy link
Author

muzea commented Jan 15, 2023

/ping @afc163 @zombieJ 这个 PR 上还有啥欠缺的东西吗?

@XianZhengquan
Copy link

@afc163 大佬,为啥这个还没有合并呢?都几年了,这个问题很严重啊

@afc163
Copy link
Member

afc163 commented Aug 18, 2025

@muzea @XianZhengquan 冲突了

@XianZhengquan
Copy link

@muzea @XianZhengquan 冲突了

@muzea 大佬,瞅一瞅呀

@Copilot Copilot AI review requested due to automatic review settings August 19, 2025 14:36
@muzea muzea force-pushed the hotfix/avoid-unnecessary-rerender branch from b505549 to 6b1ebb8 Compare August 19, 2025 14:36
Copy link

coderabbitai bot commented Aug 19, 2025

Note

Other AI code review bot(s) detected

CodeRabbit 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

Cohort / File(s) Summary of changes
Resize handling stabilization
src/Filler.tsx
将内联 onResize 替换为 useCallback 记忆的 handleResize;仅在 entry.offsetHeight 为真时调用 onInnerResize;布局/外部行为不变。
Shared config memoization
src/List.tsx
使用 useMemo 记忆 sharedConfig(只含 getKey),在 getKey 不变时保持引用稳定;无功能性变化。
Hooks: children rendering memoization
src/hooks/useChildren.tsx
用 useMemo 包裹生成子元素数组,依赖 list、startIndex、endIndex、setNodeRef、renderFunc、getKey、offsetX、scrollWidth;添加中文注释;返回结构不变。
Hooks: heights management memoization
src/hooks/useHeights.tsx
将 cancelRaf、collectHeight、setInstanceRef 用 useCallback 记忆化,明确依赖;逻辑与返回元组签名不变;新增注释说明 heightsRef 与 updatedMark 关系。
Tests: act() 包装与 RTL 调整
tests/*
tests/scroll-Firefox.test.js, tests/scroll.test.js, tests/scrollWidth.test.tsx, tests/touch.test.js, ...
将多处 scrollTo、触摸事件和 jest.runAllTimers() 包装在 React 的 act() 中;替换个别测试工具为 @testing-library/react / fireEvent,调整少量测试实现但不改变断言意图。

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
Loading
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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

小兔轻敲代码桥,
回调记忆稳如草,
子项不再徒然造,
高度采集有条跑,
测试入 act 心自妙。 🐇✨

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 Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@muzea
Copy link
Author

muzea commented Aug 19, 2025

最近事情比较多,我看看这几天 rebase 一下代码

Copy link

@Copilot 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

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 也出现变化
Copy link
Preview

Copilot AI Aug 19, 2025

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

Suggested 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 不变但是里面的数据存在变化的情况,会与之前写法存在不同的行为
Copy link
Preview

Copilot AI Aug 19, 2025

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

Suggested change
// 可能存在 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;
}, []);
Copy link
Preview

Copilot AI Aug 19, 2025

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].

Suggested change
}, []);
}, [cancelRaf]);

Copilot uses AI. Check for mistakes.

Copy link

@coderabbitai coderabbitai bot left a 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: 微调:避免重复自增 promiseIdRef

collectHeight 一开始已调用 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 5769845 and 6b1ebb8.

📒 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]);
Copy link
Member

Choose a reason for hiding this comment

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

这个 sharedConfig 里只有一个 getKey,不如把参数直接拍平,不要这层 sharedConfig 了。

@afc163
Copy link
Member

afc163 commented Aug 19, 2025

@zombieJ 也看看

@XianZhengquan
Copy link

最近事情比较多,我看看这几天 rebase 一下代码

感谢大佬 🙏

Copy link

@coderabbitai coderabbitai bot left a 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 6b1ebb8 and c973f19.

📒 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 () => …) 中,能保证最终断言观察到稳定状态。改动得当。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants