Before I invest more time into this… wanted to reach out and see if anyone can offer any quick suggestions.
I have a table with a number of rows updated by external processes (ie. ActiveJobs). These jobs run frequently at every minute, on the minute (as soon as it’s done processing) and will happen for each row in the table (there can be up to 200 rows).
I’m using turbostreams to replace (or update) each row, every minute.
I’m noticing a steady and linear growth in memory usage for both DOM (the big one) and Javascript (smaller) as the day progresses. Eventually, the Chrome tab will crashed due to an Out-Of-Memory (OOM) error.
With respect to the stimulusjs, the table has a controller, and the rows will have an action referring to the table controller (ie. data-action=“mouseover->table#onMouseOver”).
What I’ve noticed so far:
GC doesn’t seem to happen or help with the DOM, but I’ve seen the JS go down at times but there’s always creep
turbostream replace/update makes no difference
removing all stimulusjs controller and actions still seems to increase the JS memory regardless
haven’t tried using other browsers (chrome only)
I’m using the performance.measureUserAgentSpecificMemory() API as detailed here to help with tracking the memory usage programmatically:
Has anyone experienced this? This sort of memory leakage is quite demoralizing if it can’t handle this sort of volume of activity.
My next experiment is to see if I can apply a stimulusjs controller to each row and have it handle turbostream custom actions with a data payload and it will update the row’s DOM in a more surgical manner. Such thing is tedious tho. Currently I’m using a ViewComponent to render each row, so it’s nice just rendering that component and sending it over to replace the prexisting dom.
Then replacing this row doesn’t cause any memory increase in either the DOM or the Javascript.
This seems incorrect to me - and if it’s by design (or an unfortunately reality) then I think it should be documented somewhere. Unless it already is… but this to me seems pretty darn unexpected!
I have ways of getting around this but it sounds like: If you want to avoid this memory leak, any DOM snippet that is being replaced/updated cannot refer to any stimulusjs controllers outside of its scope.
In either case, main_controller.js looks like this:
import {Controller} from "@hotwired/stimulus"
export default class extends Controller {
connect() {}
disconnect() {}
doesNothing(event) {
console.log("clicked")
}
}
Using turbostreams to replace or update the div rows (many times over) will cause the DOM and JS memory to creep upwards in Example 2 but not in Example 1.
Thanks for the great report, wheee. I’m also experiencing this issue.
New DOM nodes stream in via a turbo stream broadcast (ie: job finishes). The incoming/outgoing DOM nodes are rows, same situation–either replaced or appended. Each row has a data-action defined which references a controller on an ancestor DOM element.
On our project, I don’t believe it is possible to move data-controller to every single row, as I must maintain some global state in the singleton.
This being JS and all, there are plenty of ways to work around it. I’m trying to remember exactly what I did, but it would fall under the idea of:
Instead of referencing an (external) controller in an ancestor DOM element via data-controller, you can simply reference the (external) controller and its state/actions from within the (child) controller javacript.
For example, each row would have data-action=click->childController#doSomething". The childController#doSomething would then access parentController#doSomethingForChild.
Option 1:
It looks like Stimulus has a method specifically for this type of access - If you go to Stimulus Reference - scroll to the bottom section of “Directly Invoking other Controllers”.
You can then call methods on the (parent) controller and if you want, you can even introduce getter/setter methods if you want to manipulate state.
Option 2:
Same idea as Option 1 (I did this before I knew that Stimulus had introduced the getCotnrollerForElementAndIdentifier method), but you can create your own singleton index/hash for controllers which can be accessed by any controller. (ie. think of a global variable).
Note: You can also use custom events to avoid directly coupling controllers together which can be frowned upon but it really depends on the use case. If you’re finding yourself trying to access multiple external controllers, and order-of-operations doesn’t matter, then firing a custom event is likely the better solution.
Option 3: they should really fix this memory leak… I’ll see if I can get around to raising a defect for it.