Jordan
Jordan

Reputation: 4823

Detached DOM node memory leak in React

EDIT It appears that this is due to an issue in React and is being fixed in a future release. https://github.com/facebook/react/issues/18066


Given a table in react that displays data from an API which can be refreshed with completely new information, I observed a detached DOM node leak (observe the green numbers):

Gif of memory leak

Here is the code executed in the gif (code included below for posterity). To see the leak, go to the full page, open chrome dev tools, view the "Performance Monitor" tab and click the "Regen" button quickly as seen in the gif.

In this codesandbox, where the nodes are not generated in a loop, the leak does not occur.

The only difference is the {rows} array within the jsx. The confounding part is that {rows} is not a global variable, so I don't see how it would prevent the old nodes from being GC'd.

Why does using the local variable rows result in a detached DOM node leak?

Note: The DOM Nodes seem to settle at 21,000 but there shouldn't be that many nodes anyway, as you can see it starts at 7,000 after the first table generation. In my real-world app, these detached nodes persist even through navigation (with react router) which leads me to believe that it is an actual leak and not just nodes waiting to be GC'd.


Full code simulating the leak:

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

Upvotes: 3

Views: 6984

Answers (1)

artanik
artanik

Reputation: 2704

At first, I thought this is a bug with Hooks API. Because if you replace the <FTable count={count} /> with <FTable count={1} /> then bug will gone. But this is not a solution.

There is an issue about unexpected behavior with Hooks. But in this case, instead of DOM Nodes, the JS Heap size is growing.

Then I thought "okay, I'll try this case with class component" and I did this demo. The same problem is still here. Okay, what if this problem was introduced along with Hooks in version 16.3? But, no. The same issue exist in 16.0.

And then I realize. The key question is what is common between all of these cases? The key!

Documentation says:

The best way to pick a key is to use a string that uniquely identifies a list item among its siblings.

Turned out, that React doesn't "Garbage Collect" old nodes if the key is unique on each render (well, in this case). That's why if you're using <tr key={i}> then everything is okay because React "rewrite" those nodes, and when you're using ${i * count} or ${i} ${count} or "whatever unique on every render", then nodes will be in memory. After some point, old nodes will be replaced with new ones, but I guess this is browser related behavior, not React. But I'm not a react expert and I don't know, where and how exactly this happens.

At this point, you can create an issue on GitHub and be aware of this problem.

Upvotes: 6

Related Questions