Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid memory leaks from jQuery?

jQuery holds references to DOM nodes in its internal cache until I explicitly call $.remove(). If I use a framework such as React which removes DOM nodes on its own (using native DOM element APIs), how do I clean up jQuery's mem cache?

I'm designing a fairly large app using React. For those unfamiliar, React will tear down the DOM and rebuild as needed based on its own "shadow" DOM representation. The part works great with no memory leaks.

Flash forward, we decided to use a jQuery plugin. After React runs through its render loop and builds the DOM, we initialize the plugin which causes jQuery to hold a reference to the corresponding DOM nodes. Later, the user changes tabs on the page and React removes those DOM elements. Unfortunately, because React doesn't use jQuery's $.remove() method, jQuery maintains the reference to those DOM elements and the garbage collector never clears them.

Is there a way I can tell jQuery to flush its cache, or better yet, to not cache at all? I would love to still be able to leverage jQuery for its plugins and cross-browser goodness.

like image 467
Brent Traut Avatar asked Jun 11 '15 23:06

Brent Traut


People also ask

Are there memory leaks in JavaScript?

The JavaScript engine allocates memory when you create objects and variables in your application, and it is smart enough to clear out the memory when you no longer need the objects. Memory leaks are caused due to flaws in your logic, and they make way for poor performance in your application.

Which of the following can be done to reduce memory leakage?

Use reference objects to avoid memory leaks ref package, you can work with the garbage collector in your program. This allows you to avoid directly referencing objects and use special reference objects that the garbage collector easily clears. The special subclasses allow you to refer to objects indirectly.


1 Answers

jQuery keeps track of the events and other kind of data via the internal API jQuery._data() however due to this method is internal, it has no official support.

The internal method have the following signature:

jQuery._data( DOMElement, data)

Thus, for example we are going to retrieve all event handlers attached to an Element (via jQuery):

var allEvents = jQuery._data( document, 'events');

This returns and Object containing the event type as key, and an array of event handlers as the value.

Now if you want to get all event handlers of a specific type, we can write as follow:

var clickHandlers = (jQuery._data(document, 'events') || {}).click;

This returns an Array of the "click" event handlers or undefined if the specified event is not bound to the Element.

And why I speak about this method? Because it allow us tracking down the event delegation and the event listeners attached directly, so that we can find out if an event handler is bound several times to the same Element, resulting in memory leaks.

But if you also want a similar functionality without jQuery, you can achieve it with the method getEventHandlers

Take a look at this useful articles:

  • getEventHandlers
  • getEventListeners - chrome
  • getEventListeners - firebug

Debugging

We are going to write a simple function that prints the event handlers and its namespace (if it was specified)

function writeEventHandlers (dom, event) {
    jQuery._data(dom, 'events')[event].forEach(function (item) {
        console.info(new Array(40).join("-"));
        console.log("%cnamespace: " + item.namespace, "color:orangered");
        console.log(item.handler.toString());
    });
}

Using this function is quite easy:

writeEventHandlers(window, "resize");

I wrote some utilities that allow us keep tracking of the events bound to DOM Elements

  • Gist: Get all event handlers of an Element

And if you care about performance, you will find useful the following links:

  • Leaking Memory in Single Page Apps
  • Writing Fast, Memory-Efficient JavaScript
  • JavaScript Memory Profiling

I encourage anybody who reads this post, to pay attention to memory allocation in our code, I learn the performance problems ocurrs because of three important things:

  1. Memory
  2. Memory
  3. And yes, Memory.

Events: good practices

It is a good idea create named functions in order to bind and unbind event handlers from DOM elements.

If you are creating DOM elements dynamically, and for example, adding handlers to some events, you could consider using event delegation instead of keep bounding event listeners directly to each element, that way, a parent of dynamically added elements will handle the event. Also if you are using jQuery, you can namespace the events ;)

//the worse!
$(".my-elements").click(function(){});

//not good, anonymous function can not be unbinded
$(".my-element").on("click", function(){});

//better, named function can be unbinded
$(".my-element").on("click", onClickHandler);
$(".my-element").off("click", onClickHandler);

//delegate! it is bound just one time to a parent element
$("#wrapper").on("click.nsFeature", ".my-elements", onClickMyElement);

//ensure the event handler is not bound several times
$("#wrapper")
    .off(".nsFeature1 .nsFeature2") //unbind event handlers by namespace
    .on("click.nsFeature1", ".show-popup", onShowPopup)
    .on("click.nsFeature2", ".show-tooltip", onShowTooltip);

Circular references

Although circular references are not a problem anymore for those browsers that implement the Mark-and-sweep algorithm in their Garbage Collector, it is not a wise practice using that kind of objects if we are interchanging data, because is not possible (for now) serialize to JSON, but in future releases, it will be possible due to a new algorithm that handles that kind of objects. Let's see an example:

var o1 = {};
    o2 = {};
o1.a = o2; // o1 references o2
o2.a = o1; // o2 references o1

//now we try to serialize to JSON
var json = JSON.stringify(o1);
//we get:"Uncaught TypeError: Converting circular structure to JSON"

Now let's try with this other example

var freeman = {
    name: "Gordon Freeman",
    friends: ["Barney Calhoun"]
};

var david = {
    name: "David Rivera",
    friends: ["John Carmack"]
};

//we create a circular reference
freeman.friends.push(david); //freeman references david
david.friends.push(freeman); //david references freeman

//now we try to serialize to JSON
var json = JSON.stringify(freeman);
//we get:"Uncaught TypeError: Converting circular structure to JSON"

PD: This article is about Cloning Objects in JavaScript. Also this gist contain demos about cloning objects with circular references: clone.js


Reusing objects

Let's follow some of the programming principles, DRY (Don't Repeat Yourself) and instead of creating new objects with similar functionality, we can abstract them in a fancy way. In this example I will going to reuse an event handler (again with events)

//the usual way
function onShowContainer(e) {
    $("#container").show();
}
function onHideContainer(e) {
    $("#container").hide();
}
$("#btn1").on("click.btn1", onShowContainer);
$("#btn2").on("click.btn2", onHideContainer);
 
//the good way, passing data to events
function onToggleContainer(e) {
    $("#container").toggle(e.data.show);
}
$("#btn1").on("click.btn1", { show: true }, onToggleContainer);
$("#btn2").on("click.btn2", { show: false }, onToggleContainer);

And there are a lot of ways to improve our code, having an impact on performance, and preventing memory leaks. In this post I spoke mainly about events, but there are other ways that can produce memory leaks. I suggest read the articles posted before.


Happy reading and happy coding!

like image 138
jherax Avatar answered Sep 21 '22 05:09

jherax