【问题标题】:Detached DOM node memory leak in ReactReact 中分离的 DOM 节点内存泄漏
【发布时间】:2026-01-04 03:20:03
【问题描述】:

编辑 这似乎是由于 React 中的一个问题,并在未来的版本中得到修复。 https://github.com/facebook/react/issues/18066


鉴于 react 中的表格显示来自 API 的数据,可以使用全新的信息进行刷新,我观察到分离的 DOM 节点泄漏(观察绿色数字):

Here is the code executed in the gif(代码包含在下面以供后代使用)。 要查看泄漏,请转到full page,打开 chrome 开发工具,查看“性能监视器”选项卡并快速单击“重新生成”按钮,如 gif 所示。

In this codesandbox,节点不是在循环中生成的,泄漏确实不会发生。

唯一的区别是 jsx 中的 {rows} 数组。令人困惑的部分是 {rows} 不是全局变量,所以我看不出它如何防止旧节点被 GC。

为什么使用局部变量 rows 会导致分离的 DOM 节点泄漏?

注意:DOM 节点似乎稳定在 21,000,但无论如何不应该有那么多节点,因为您可以看到它在第一个表生成后从 7,000 开始。在我的真实应用程序中,这些分离的节点甚至通过导航(使用反应路由器)仍然存在,这让我相信这是一个实际的泄漏,而不仅仅是等待 GC 的节点。


模拟泄漏的完整代码:

import React, { useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);
  return (
    <div className="App">
      <button onClick={() => setCount(prev => prev + 1)}>Regen</button>
      <FTable count={count} />
    </div>
  );
}

function Cell() {
  const num = Math.floor(Math.random() * 100);
  return <td>{num}</td>;
}
function FTable(props) {
  const { count } = props;
  const rows = [];
  if (count > 0) {
    for (let i = 0; i < 1000; i++) {
      rows.push(
        // Use a different key for each time the
        // table is regenerated to simulate a new API
        // call bringing in new data
        <tr key={`${i} ${count}`}>
          <Cell row={i} />
          <Cell row={i} />
          <Cell row={i} />
        </tr>
      );
    }
  }
  return (
    <div>
      <table>
        <tbody>{rows}</tbody>
      </table>
    </div>
  );
}

【问题讨论】:

  • 你在本地试过吗?我想知道这是否可能是由于代码沙箱上的某种无限循环保护引起的?
  • @JayB 是的,问题首先发生在本地

标签: javascript reactjs memory-leaks react-hooks


【解决方案1】:

起初,我认为这是 Hooks API 的一个错误。因为如果您将&lt;FTable count={count} /&gt; 替换为&lt;FTable count={1} /&gt;,那么错误就会消失。但这不是解决方案。

关于带有 Hooks 的 unexpected behavior 存在问题。但在这种情况下,JS Heap 的大小正在增长,而不是 DOM 节点。

然后我想“好吧,我会用类组件试试这个案例”,我做了this demo。同样的问题仍然存在。好的,如果这个问题是在 16.3 版本中与 Hooks 一起引入的呢?但不是。同样的问题exist in 16.0

然后我意识到。关键问题是所有这些案例之间的共同点是什么?关键!

Documentation says:

选择键的最佳方法是使用一个字符串,该字符串在其兄弟项中唯一标识一个列表项。

事实证明,如果密钥在每个渲染中都是唯一的,React 不会“垃圾收集”旧节点(在这种情况下)。这就是为什么如果你使用 &lt;tr key={i}&gt; 那么一切都好,因为 React "rewrite" 那些节点,当你使用 ${i * count}${i} ${count}"任何独特的每次渲染”,那么节点将在内存中。过了一段时间,旧节点将被新节点替换,但我想这是与浏览器相关的行为,而不是 React。但我不是反应专家,我不知道这究竟发生在哪里以及如何发生。

此时,您可以在 GitHub 上创建问题并注意此问题。

【讨论】: