Caching jQuery Objects

In much of our legacy JavaScript code jQuery objects are not cached. This is a performance concern that can be easily mitigated by some simple refactoring.

Every time you use the jQuery object to dereference a selector, jQuery traverses the DOM to find the element in question. Consider the following code:

$('#foo').on('click', () => {
    $('#foo').toggleClass('disabled');
    if ($('#foo').is(':disabled')) {
        $('#foo').css({ color: 'silver' });
    } else {
        $('#foo').css({ color: 'blue' });
    }
});

Regardless of whether #foo is actually disabled, when this event handler is executed jQuery will have scanned the DOM four times to find the #foo element–

  1. once to assign the click handler
  2. once to toggle the class
  3. once to determine if the element is disabled
  4. once to either assign the color silver or blue to the element

Each scan is a very minor performance concern, but as the lines of JS multiply, additional jQuery scans can bog down a page–especially on mobile devices.

The solution is to cache a jQuery object by assigning it to a variable, then referencing that variable when subsequent jQuery operations are required. The example above can be re-written:

const $foo = $('#foo');
$foo.on('click', () => {
    $foo.toggleClass('disabled');
    if ($foo.is(':disabled')) {
        $foo.css({ color: 'silver' });
    } else {
        $foo.css({ color: 'blue' });
    }
});

In this case, the $foo variable is created when jQuery acquires the #foo element from the DOM. This happens once, and only once. The variable is then used to manipulate the element classes, CSS attributes, etc. Another benefit to this approach is that it simplifies refactoring. It is much easier to rename a single variable than to hunt and peck for every instance of a given DOM selector sprinkled throughout a JS module!

A related performance optimization is searching for child elements within cached jQuery objects instead of searching the entire DOM for them. Consider the following markup:

<section class="foo">
    <section class="messages"><!-- messages will go here --></section>
    <ul class="bar">
        <li>
            <a href="#" class="baz">Baz!</a>
            <a href="#" class="baz">Bin!</a>
            <a href="#" class="baz">Buzz!</a>
        </li>
    </ul>
</section>

In this situation, whenever a .baz anchor is clicked, we want to display the text of the element within .messages. We could write the JS like this:

$(() => {
    const $messages = $('.messages');
    const $bazzes = $('.baz');
    $bazzes.on('click', (e) => {
        $messages.text($(e.target).text());
    });
});

In this case, both .messages and .baz are located by traversing the entire DOM, and the list cannot be recreated without manually removing and re-adding the event listener from the set of list item elements. If, however, we write our JS like this,

$(() => {
    const $foo = $('.foo');
    const $messages = $foo.find('.messages');
    $foo.on('click', '.baz', (e) => {
        $messages.text($(e.target).text());
    });
});

we accomplish two things:

  1. we use the $foo variable to find the .messages element, which is much faster since .messages is a direct descendent of .foo; and
  2. we use the $foo element to listen for events that are raised by .baz elements, then handle them accorrdingly.

This form of event handling is called event delegation because we allow a parent element to deal with events that child elements raise. If we ever have to re-create the list items in the DOM, we don’t need to worry about removing/adding the event handler again because it is attached to a parent element and not to the list items themselves.

Small improvements like these can help us keep code clean, maintainable, and performing well.