letsgetsilly
letsgetsilly

Reputation: 1196

Caching jquery selector results using underscore memoize

I'm looking into ways to optimize my Single Page Application, and I'm currently focusing on jQuery selectors.

I'm no javascript expert, but it's my understanding that it's much more performant to store the results of a jquery selector in a variable for re-use, as opposed to re-querying the dom.

So, for example, instead of this:

$("#myItem").doSomething();
$("#myItem").doSomethingElse();

It makes more sense to do this:

var result = $("#myItem");
result.doSomething();
result.doSomethingElse();

Also, it's my understanding there can be great performance benefits for using the find() selector attached to an existing jQuery object as to reduce the amount of querying necessary:

$("#myItem").find(".highlighted");

I am interested in exploring the possibility of storing many of the selectors that we constantly reuse in our application not just in a local variable, but possibly in a hash somewhere, or an external function. I don't know if this would optimal, but I'm curious to what responses this receives.

I've attempted (unsuccessfully) to use Underscore.js' memoize function to perform this, but it isn't working the way I expected. But, the concept will look like this:

   jquerySelect = function () {
                var elements = _.memoize(function (selection) {
                    return $(selection);
                });
                return elements;
            };

The idea here being that the actual result work of the "return $(selection)" be performed once, after which the result is cached and returned.

I'd like to be able to use this in the following way:

utils.jquerySelect(".highlighted").find("span")

In this simple example, the key to the cache is the ".highlighted", and the utility function knows how to access the result, thus saving the work of traversing the DOM from being done again. My implementation doesn't work currently, as the "return" statement is hit every time.

I'm not sure if this approach is flawed, but I sense it may be. However, please let me know if you see a way for this to work, or if you see a better way of doing this.

Upvotes: 3

Views: 1461

Answers (5)

Sudhanshu Yadav
Sudhanshu Yadav

Reputation: 2333

Caching is the best practice to follow in jquery.

I divided caching into two categories.

1. local cahching : whose scope is inside some function only.

ex:

function doSome(){
var elm=$('selector');
elm.something1().something2()  .... chain it on one
}

Points to keep on mind while use :

i. to cache elements which are accessed by very few function or say by that function only.

ii. When elements are added dynamically than you need to reinitialize it every time so local caching are better.

iii. keep all var declaration at top along with variables which are used for caching.

iv. cache once and filter later using .filter('selector') or .filter(function) ex. elm.filter(':checked').dosomething();

2. Global Caching Whose scope is to all functions

for global caching declare a object and make all caching as key value pair in that variable.

ex:

var global={};
$(document).ready(function(){
global.sel1=$('selector1');
global.sel2=$('selector2');
});

//remeber always add cahing variable inside document ready function if you are declaring global variable on the top of page. As elements will not be present in dom so selectors will return undefined.

if you are puting all scripts on down of the page which i prefer than you can use

var global={
sel1:$('selector1'),
sel2:$('selector2')
} ;

points to remember

  1. Wrapper elements are best to cache in global caching. As child elements you can find using .find() and are generally not modified practically. (i will suggest to keep all wrapper element with ids as id is the fastest way of selection.)
  2. Use only for frequently used elements. dont use it for selector for which element will be appended later .
  3. Create a local reference of your global object inside a function as accessing global things is costlier than accessing local things. ex: function some(){ var elm=global.sel1; //now do something with elm }

A setter and getter function for global chaching

function getGlobal(selector){
//if not present create it .
if(!global[selector]){
global[selector]=$(selector);
}
return global[selector];
}

//to use it
var elm=getGlobal('selector');

Upvotes: -1

mikakun
mikakun

Reputation: 2265

i'd say It makes more sense to do this:

var myapp={};
myapp.$body=$(document.body);
myapp.$result = myapp.$body.find("#myItem").doSomething().doSomethingElse();
myapp.$result.find("> .whatever")

about storing large object, if you mean to be reused throughout a session, i've been looking into it & found that local store allow for plain static object only so won't accept myapp.$result for example, then i gave up on the idea.

Upvotes: 0

Plynx
Plynx

Reputation: 11461

It's not a bad idea, and in fact you can find dozens of similar memoization plugins for JQuery out there already. The major challenge with your approach is knowing when you can use a memoized result, and when it is invalidated due to changes in the DOM. Since JQuery code is just as likely to add, remove, and change nodes in the DOM as they are to select from it, knowing when you must invalidate a selector from your memoized cache is a non-trivial problem.

The way memoize plugins work around this is usually by providing some alternate syntax, such as $_() for selectors you want to be memoized and/or from which you want to retrieve the memoized results. Regardless, though, this adds an additional string/hash lookup to the selector. So, in the end, what's the true advantage of this over caching locally?

Generally speaking, it's better to learn more about how JQuery is essentially a DSL providing monadic transformations to the DOM (also known as chaining).

For example, your code in the question can be written:

$("#myItem").doSomething().doSomethingElse();

You can even use .end() to pop the selector stack, since in a chain results are internally memoized already:

$("#myItem").doSomething().find('a').hide().end().doSomethingElse();
// a in #myItem are hidden, but doSomething() and doSomethingElse() happen to #myItem

Also, the cost of most JQuery selections can be overestimated. ID selects (selectors that begin with #) are passed to fast DOM methods like document.getElementById instead of the Sizzle engine. Repeating calls to selectors that resolve to a single DOM element, e.g. $(this) are of no particular consequence to speed.

If speed is really the concern in a tight loop in your code, it's almost certainly worth it to refactor that section out of using the JQuery selection engine entirely. In my tests, you can get a 10-50% performance gain from local selector caching, depending on the complexity of the selector, and 1000% performance gain from native DOM selectors over JQuery selectors.

Upvotes: 3

James Montagne
James Montagne

Reputation: 78650

The overall idea is a good one. As stated by others already though, you need to be careful you are not caching things which specifically need to be re-queried.

The reason your attempt failed is that you are using _.memoize incorrectly. It would look like this:

var jquerySelect = _.memoize(function (selection) {
                       return $(selection);
                   });

memoize returns a function that you would then call. It essentially creates a wrapper around your function that handles the caching.

Upvotes: 0

joeltine
joeltine

Reputation: 1630

I can speak specifically about memoize, but I can say that your strategy of maintaining an associative array of cached jQuery collections is a good idea. With that being said, you need to take a few things into consideration:

  1. It may not always be worth it to cache a selector that will be used only 0 or 1 times up front.
  2. If you're trying to calculate something dynamic about a collection of elements (for example length), you will need to re-select/re-cache the element before getting the value. For example:

    var $div = $("div");

    $div.length; //say it's 1

    //...some code that adds some divs to the page

    $div.length; //still 1!

  3. I would consider doing your caching "in-time". In other words, only cache the item the first time it's needed.

Upvotes: 0

Related Questions