Skip to content
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

Memory Leak - React DOM keeps references to stale child components when using the Container/Pure component pattern #18790

Closed
jzworkman opened this issue Apr 30, 2020 · 19 comments

Comments

@jzworkman
Copy link

Do you want to request a feature or report a bug?
Report a bug.

What is the current behavior?
ReactDOM keeps references to previous states/props/children when component gets updated. All in all consuming three times as much memory as it really needed.

We are seeing this as a significant issue when using Redux and container components. When our container componet(that is connected to redux store) passes props to the child components, and then the redux store updates. The child component props are being stranded in the dom with old reference(seen in the heap after forcing a garbage collection cycle). This is causing huge amounts of memory bloat when on a page that is connected to signalR for real time collaboration between users(as each redux update creates hundreds of stranded object references in the child components).

I have verified that this is the cause by instead having all of the previously "dumb" pure child components be converted to Connected components and pull their props from redux instead of having the container component pattern control all of the store connections. this then correctly all references the single redux store object and garbage collection works as expected without stranded references.

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:

Link to the example below (using production versions of react and react-dom):
https://codesandbox.io/s/epic-bartik-pvgqx.

Consider following example:

import * as React from 'react';
import * as ReactDOM from 'react-dom';

let dataInstanceCount = 0;
class MyBigData {
  constructor() {
    const id = `my-big-data:${dataInstanceCount++}`;
    this.getMyDataId = () => id;
    this.data = new Array(100000).fill('');
  }
}

let componentInstanceCount = 0;
class MyItem extends React.Component {
  constructor(props) {
    super(props);
    this._myItemId = `my-item:${componentInstanceCount++}`;
    this.state = {list: []};
  }

  render() {
    return this.props.item.getMyDataId();
  }
}

class MyApp extends React.Component {
  constructor(props) {
    super(props);
    this.state = {list: []};
  }

  componentDidMount() {
    this.updateList(() => {
      this.updateList(() => {
        this.updateList();
      });
    });
  }

  updateList(callback) {
    this.setState({
      list: [new MyBigData()]
    }, callback);
  }

  render() {
    return this.state.list.map((item) => (
      <MyItem key={item.getMyDataId()} item={item} />
    ));
  }
}

const rootElement = document.getElementById('root');
ReactDOM.render(
  <MyApp />,
  rootElement
);

setTimeout(() => {
  console.log(
    rootElement._reactRootContainer._internalRoot.current.alternate.firstEffect.memoizedProps.item.getMyDataId(),
    rootElement._reactRootContainer._internalRoot.current.alternate.firstEffect.stateNode._myItemId
  );
  // > my-big-data:0, my-item:0
  console.log(
    rootElement._reactRootContainer._internalRoot.current.firstEffect.memoizedProps.item.getMyDataId(),
    rootElement._reactRootContainer._internalRoot.current.firstEffect.stateNode._myItemId
  );
  // > my-big-data:1, my-item:1
  console.log(
    rootElement._reactRootContainer._internalRoot.current.lastEffect.memoizedProps.item.getMyDataId(),
    rootElement._reactRootContainer._internalRoot.current.lastEffect.stateNode._myItemId
  );
  // > my-big-data:2, my-item:2
}, 1000);

I expect only one MyBigObject and one MyItem component to be in the memory. But instead I can see three of each in memory heap snapshot.

UPDATE
As shown in the updated example the references to these objects and components can be accessed in the sub-properties of the root DOM element.

What is the expected behavior?
There's no justifiable reason to keep in memory unmounted components and previous states/props of component after it was updated.

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
React 16.9.0, ReactDOM 16.9.0 (Production versions)
Mac/Win

This info was gathered from the follow issue that was marked stale, but is still definitely an issue with Redux and passing store props to Pure Child unconnected components: #16138

@cliffhall
Copy link

I’m seeing similar issue but using Hooks / useState and functional components. We have an infinite scroll and a buffer window. After the buffer is full, old components are removed and new ones added, with the size of the buffer being all that should exist in memory at any one time. But memory just keeps rising. There are no setTimer or event listeners or other in app or global objects keeping references to those discarded components, yet they live on.

@gaearon
Copy link
Collaborator

gaearon commented May 4, 2020

I'm a bit confused about the example. You're saying the problem is with using Redux and "pure" components, but then the example uses neither. Can you clarify what you mean by that?

@jzworkman
Copy link
Author

@gaearon Sorry this post had two separate examples, the code example posted was from the Original post(that was closed for being stale despite still being an issue). The Redux example is from my personal experience with the same type of memory leak(that has a work around).
IE Redux state passed to a pure unconnected child component from a parent leaks that memory on redux state update/re-render. However, if the child component is also connected to the redux store directly and getting its Props from redux instead of from the Parent, then the garbage collection works correctly and the memory is cleaned up on subsequent renders.

The root of the issue is that when Props/State are updated in the parent component and passed as props to the child component, the child is not correctly disposing of the references to allow garbage collection to occur on future renders.

There is a lot more information in the chain of issue #16138

@gaearon
Copy link
Collaborator

gaearon commented May 4, 2020

There's no justifiable reason to keep in memory unmounted components and previous states/props of component after it was updated.

This is really strongly worded! :D

Yes, it's not ideal, but in some cases we do keep a previous tree in memory — until the next time the component updates. So I'd only say it's a leak if it keeps growing regardless of parent component updates. These tradeoffs could change though (see #16087 for details).

@gaearon
Copy link
Collaborator

gaearon commented May 4, 2020

@cliffhall Your description isn't very helpful without a code sample. What you're describing does sound like a leak but we can't guess what it is unless you reduce it.

@cliffhall
Copy link

cliffhall commented May 4, 2020 via email

@jzworkman
Copy link
Author

Yes, it's not ideal, but in some cases we do keep a previous tree in memory — until the next time the component updates. So I'd only say it's a leak if it keeps growing regardless of parent component updates.

Yea this is the key part we were seeing, that when getting updates to the parent through Redux(we were getting real time updates of a large accounting report via SignalR that would update the parent). This would result in 10+ abandoned trees in the stack, and they were not cleared by forcing a garbage collection cycle.

When I instead hooked the child display components directly up to redux instead of the parent container, then the previous snapshots were correctly garbage collected

@gaearon
Copy link
Collaborator

gaearon commented May 4, 2020

The original code example doesn't repro for me with 16.3.1: https://codesandbox.io/s/dark-silence-ypgum?file=/src/index.js

So we'll need an actual repro case to look into this.

@dmitrysteblyuk
Copy link

dmitrysteblyuk commented May 14, 2020

There's no justifiable reason to keep in memory unmounted components and previous states/props of component after it was updated.

This is really strongly worded! :D

That would be me, sorry!
I can confirm that the original repro doesn't work anymore.
Please check out this new one react-memory-leak-repro.zip

Basically you can modify the list of items (each containing big data) - and whatever you do (removing all, adding then removing etc) there're just still too many stale props in memory.

@EvertEt
Copy link

EvertEt commented Jun 14, 2020

Looking at the repro above (https://codesandbox.io/s/zealous-golick-s7uue?file=/src/index.js), it reminds me of the fix in #15157 regarding stateNode that wasn't ported to #16115 which did get merged.

@cliffhall
Copy link

@gaearon I finally tracked down my memory leak. It was occuring in the react-media npm package. So, not related to this thread. My bad.

@jljorgenson18
Copy link

jljorgenson18 commented Sep 2, 2020

Not sure if this helps but we have a bad memory leak related to storing callbacks in Redux that are created in custom hooks. The objects are definitely cleared inside of Redux, but those callbacks being created in the custom hooks seem to hold a reference somewhere and keep the entire render tree around. If I take out the callback, the memory leak goes away. I'm not positive if this is a React problem or a React-Redux problem though.

Nvm, it was the redux logger

@navinv789
Copy link

Even we are facing the same issue.
Any workaround available for this?

@Venryx
Copy link

Venryx commented Nov 12, 2020

I have reproduced this memory leak in a very small and self-contained demo.

Demo source: https://codesandbox.io/s/reactjs-useeffect-memory-leak-8iv16?file=/src/index.tsx
Demo standalone page: https://8iv16.csb.app

Leak reproduction instructions:

  1. Open the demo standalone page. (needed for consistent memory profiler data)
  2. Open dev-tools, and go to Memory tab.
  3. Press the garbage-can icon at top-left, to run GC. Memory-usage shown for "https://8iv16.csb.app" VM is the baseline. (it can vary, eg. having the demo-editor open in another tab increases it by ~50mb)
  4. Switch render-type to "Heavy", and run GC. VM usage will increase to baseline + 60mb (1 array copy).
  5. Disable "Show contents", and run GC. VM usage will go back down to baseline. (0 array copies) [repeat steps 4 and 5 if desired]
  6. Ensure "Show contents" is disabled, switch render-type to "Container->heavy", and run GC. VM usage should remain at baseline. (0 array copies)
  7. Enable "Show contents", and run GC. VM usage will increase to baseline + 60mb. (1 array copy - no leak yet)
  8. Disable "Show contents", and run GC. VM usage will stay at baseline + 60mb! (1 array copy - leak!)
  9. Enable "Show contents", and run GC. VM usage will increase to baseline + 120mb! (2 array copies - leak!) [repeat steps 8 and 9 if desired]

Here is a screen capture showing me performing the steps above, with exactly the results described:

The issue seems to be that -- for certain patterns of component trees (eg. Container->HeavyChild in demo, but not HeavyChild directly) -- React holds onto the reference to the "cleanup function" returned by the useEffect hook, even after the earlier component instance has been unmounted!

(You can confirm that the "cleanup function" of the useEffect hook is the leak-path specifically, because if you comment out that cleanup-function, the problem of encountering the "2 array copies" state is resolved. In fact, you don't even encounter the "1 array copy" state anymore [after running GC], as nothing ever forms a func-external reference to the array.)

While the memory leak's effects are not that serious in this demo, it can have major memory-usage ramifications in cases where the "cleanup function" of the useEffect hook references variables holding larger amounts of data. In my app specifically, the cleanup function held a reference to a very large data-array that was sent into the component. Even after the component was unmounted (/remounted with a new data-array), the useEffect hook's "cleanup function" was holding onto the old array values!

This was causing major memory issues for my app, as the data-array was so large that holding even just a couple more than expected, was putting the app in range of out-of-memory errors. (criticize this as you like, but it's not easy storing 256+256+64+64+64 number values per second, for an entire night, in frontend memory!)

Anyway, the point is not my specific app's issues, but the fact that the memory leak exists in general.

Venryx added a commit to Venryx/react-uplot that referenced this issue Nov 13, 2020
* Tried to add workaround for ReactJS memory-leak issue. (where variables referenced in useEffect cleanup function can leak, by means of the cleanup-function ref being retained!) [see: facebook/react#18790 (comment)]

Workaround didn't seem to work for every case though, so I fell back to a userland fix. (setting array.length to 0, for the old passed "data" array, so that even though leaked reference remains, it has no significant memory cost)
@AndyJinSS
Copy link

This issue has been fixed in the new version(16.13(+), 17+).

@pbu-genus
Copy link

We are getting a similar issue with React Portal in version 17.0.2. I am not sure if it is the exact same issue, or if we should create a new one.

State and DOM elements created inside ReactDOM.createPortal seems to remain in memory after forced GC.

We are using react/reactDOM 17.0.2.

All we need is to toggle the portal.
When doing memory snapshots, it can be observed that the elements created inside createPortal persists even after forced GC.
This also applies for state. I will include two codepens, one with the minimum requirements and one where state is also preserved.

The memory leak can be seen by looking for detached HTMLParagraphElements in the minimum example, and it can also be observed that memory reserved by arrays deterministically increases with each toggle in the second codepen.

(The issue is only visible in debug view)
Here is a minimum CodePen showcasing the issue:
https://codepen.io/pbu_genus/pen/qBVKygK

And here is one with state preserved:
https://codepen.io/pbu_genus/pen/LYOrOYj

All screenshots below are taken after forced GC:
After 2 toggles (min case):
image
After 2 toggles (second case):
image

After 4 toggles (min case):
image
After 4 toggles (second case):
image

@salazarm
Copy link
Contributor

@pbu-genus Can you check what is holding references to those objects? It should tell you why its being retained. Could it be dev tools?

@pbu-genus
Copy link

@salazarm Ah, my bad, disabling react dev tools solved the issue!

@gaearon
Copy link
Collaborator

gaearon commented Mar 30, 2022

It looks like the issue got resolved? We've also made cleanup more aggressive in React 18 which might help.

If it still reproduces, please lmk and I'll reopen.

@gaearon gaearon closed this as completed Mar 30, 2022
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

No branches or pull requests