After weeks of effort, a team mate finally figured out that a default noop function param, eg function blah( param = () => {} ), was never being gc’d. Wut?!
I recently wrote an automatic memory leak detector and debugger, which makes this a lot easier (imo) [0]. You write a short input script that drives the UI in some loop, it looks for growing things (objects, arrays, event listener lists, DOM node lists...), and then collects stack traces to find the code that grew them. While it won't find all of the leaks, I was able to eliminate an average of 94% of the live heap growth I observed in 5 web applications (which found new memory leaks in Google Analytics, AngularJS, Google Maps, etc).
More information about the technique can be found in a PLDI paper (which I presented last week :D ), which I tried to write clearly so that it is accessible to a technical audience (i.e., non-academics) [1].
I once used the Python function "gc.get_objects()", which returns a list of all objects in memory, to diagnose a memory leak. I don't suppose anything like it exists in JS tooling?
You can capture a heap snapshot in most browsers now using development tools. However, even a blank webpage (about:blank) has tens of thousands of objects allocated for the default JavaScript/DOM APIs. It's challenging to manually grok a JavaScript heap.
The approach I used was to take a snapshot, count up all the different object types (making a hash table mapping "name of type of object" => "count of objects whose type has that name", then discarding the snapshot), then take another snapshot a few minutes later, and see what the difference was, and which type of object there was suddenly a lot more of. Then I looked at various instances of that object. It turned out to be some async queue-related object that was only used in a couple of places, so that narrowed it down a lot. Even if it were something generic like a hash table or list, I suspect looking at instances of the object and breaking them down by some observable quality (e.g. number of elements, the set of keys in a table, the types of objects in a list), plus the differential approach, will take you fairly far.
There are some browser-specific tools that will do something like that out-of-band (e.g. in Firefox about:memory has a "Save GC & CC logs" option that outputs data about what the GC and cycle collector heap graphs look like). But interpreting those graphs is not easy, sadly.
After weeks of effort, a team mate finally figured out that a default noop function param, eg function blah( param = () => {} ), was never being gc’d. Wut?!