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

Clear dangling EventListeners and Detached Nodes when a controller is removed from the DOM #1

Closed
wants to merge 1 commit into from

Conversation

intrip
Copy link
Member

@intrip intrip commented Oct 19, 2022

eventListenerMaps maps HTML elements handled by Stimulus to eventListenerMap, a map of Stimulus EventListeners in this way: { HTMLelement: { eventName: EventListener } }. When a controller HTML is removed from the DOM the dangling EventListeners aren't removed, moreover, the removed HTML elements are still referenced via the eventListenerMaps keys. This leads to having multiple detached HTMLelements and eventListeners which can't be GCed hence a memory leak. The leak is fixed by removing the dangling eventListeners and clearing unused eventListenerMaps keys. When the HTMLelement is attached again to the DOM, the eventListenerMaps and the related eventListeners are automatically re-created by Stimulus via the MutationObservers, no data is lost by doing this.

Below is an example to reproduce the issue:

<html>
  <head>
    <title>EventListenerMaps memory leak</title>
    <script type="module">
      import { Application, Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"
      window.Stimulus = Application.start()
      Stimulus.register("boosts", class extends Controller {
        doClick() { process(500) }
      })
    </script>
    <script>
      function process(count) {
        let i = 0
        let handler = setInterval(function(){
          if (++i > count) {
            clearInterval(handler)
          } else {
            document.body.replaceWith(document.body.cloneNode(true))
          }
        }, 1)
      }
    </script>
</head>
<body>
  <div data-controller="boosts">
    To reproduce:
    <ul>
      <li>Check heap snapshot</li>
      <li>Click "trigger leak" button</li>
      <li>Check heap snapshot again</li>
    </ul>
    <button data-action="click->boosts#doClick">trigger leak</button>
  </div>
</body>
</html>

heap snapshot before:

Screenshot 2022-10-19 at 17 10 35

heap snapshot after:

Screenshot 2022-10-19 at 17 11 10

Related hotwired#489 hotwired#547

… removed from the DOM

`eventListenerMaps` maps HTML elements handled by Stimulus to `eventListenerMap`, a map of Stimulus
EventListeners in this way: `{ HTMLelement: { eventName: EventListener } }`.
When a controller HTML is removed from the DOM the dangling EventListeners aren't removed,
moreover, the removed HTML elements are still referenced via the `eventListenerMaps` keys.
This leads to having multiple detached HTMLelements and eventListeners which can't be GCed
hence a memory leak. The leak is fixed by removing the dangling eventListeners and clearing
unused `eventListenerMaps` keys. When the HTMLelement is attached again to the DOM,
the `eventListenerMaps` and the related `eventListeners` are automatically re-created
by Stimulus via the MutationObservers, no data is lost by doing this.

Below is an example to reproduce the issue:

```
<html>
  <head>
    <title>EventListenerMaps memory leak</title>
    <script type="module">
      import { Application, Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"
      window.Stimulus = Application.start()
      Stimulus.register("boosts", class extends Controller {
        doClick() { process(500) }
      })
    </script>
    <script>
      function process(count) {
        let i = 0
        let handler = setInterval(function(){
          if (++i > count) {
            clearInterval(handler)
          } else {
            document.body.replaceWith(document.body.cloneNode(true))
          }
        }, 1)
      }
    </script>
</head>
<body>
  <div data-controller="boosts">
    To reproduce:
    <ul>
      <li>Check heap snapshot</li>
      <li>Click "trigger leak" button</li>
      <li>Check heap snapshot again</li>
    </ul>
    <button data-action="click->boosts#doClick">trigger leak</button>
  </div>
</body>
</html>
```
@intrip intrip force-pushed the clear-dangling-event-listeners branch from 1816cca to 1dc5415 Compare October 19, 2022 15:20
@intrip intrip closed this Oct 20, 2022
@intrip intrip reopened this Oct 20, 2022
@intrip intrip closed this Oct 20, 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
1 participant