siteground

Despite the grow of JavaScript frameworks, jQuery still is the most popular library used in normal web apps. We use jQuery heavily in our Meta Box plugin for WordPress. One of major problems that we face when using jQuery for the clone feature is the slowness. While cloning, the plugin does a lot of work such as creating a new DOM element, adjust the input index, reset the values, etc. For simple fields, these actions are quite fast. But for groups, it takes longer, especially for groups with many sub-fields and sub-groups. While looking for a solution for this problem, I found that it’s not too hard to fix. The simple trick that I applied helps increase the speed up to 800%! That’s so amazing.

Identify the problem

This is the screenshot of the Meta Box plugin. Clicking on Add More button will add another author to the book. Each author has some info like first name, last name and biography.

jquery selector performance
Cloning group of custom fields in Meta Box

There are sequence of jobs while cloning:

  1. Find the last instance, create a new clone of that and insert it to the bottom of the list.
  2. Inside the clone instance, find all inputs and change their index and reset their values to empty string.

The first job seems to be not performant as it has to work with DOM. Creating a clone and insert it into the page is always a painful job. But we have no choice. It has to be done like that. There’s simply no better way to handle it. Unless we use a JavaScript framework like React, but we don’t.

After looking at those problems, I found that the 2nd problem causes the slowness. And among those actions, the job of finding all inputs is one of the major issue.

We used to use this code to find all inputs made by Meta Box:

$clone.find( ':input[class*="rwmb"]' ).each( function () {
   // Adjust the [name] attribute
   // Adjust the [id] attribute
} );

(I shorten the code to make it easier to understand the issue. For full code, please see here).

Can you see the selector? :input[class*="rwmb"]. It selects all the inputs (input, select, textarea, button) with the CSS class contains string rwmb (an identify we use for Meta Box).

This selector is a custom jQuery selector, e.g. it’s created by jQuery and available in jQuery only. According to jQuery documentation:

Because :input is a jQuery extension and not part of the CSS specification, queries using :input cannot take advantage of the performance boost provided by the native DOM querySelectorAll() method.

And that is the root of the problem! Solution: just change the selector!

Optimizing jQuery selector

Based on the jQuery documentation, it’s recommended to use native CSS selector that function querySelectorAll() supports. In that case, jQuery will utilize the native function, without looking into the DOM tree and search by itself.

So, we need to select all inputs with the CSS class contains string rwmb. We can list all the available selectors like this:

  • input[class*="rwmb"]
  • select[class*="rwmb"]
  • textarea[class*="rwmb"]
  • button[class*="rwmb"]

These are CSS attribute selectors and are supported in CSS3. There are more attribute selectors that you can find here.

So I rewrote the code as follows:

var inputSelectors = 'input[class*="rwmb"], textarea[class*="rwmb"], select[class*="rwmb"], button[class*="rwmb"]';
$clone.find( inputSelectors ).each( function () {
    // Adjust the [name] attribute
    // Adjust the [id] attribute
} );

Logically, it should work as expected. But does it?

Testing new CSS attribute selectors

To make sure it runs as expected and to measure the performance, I make a test on jsperf.com. Please visit the link to see full test code. Basically it does the following:

  • Insert 1000 inputs to body
  • Insert 1000 textareas to body
  • Insert 1000 selects to body
  • Insert 1000 buttons to body
  • Then select them with :input or CSS attribute selector

I also added a test to selecting all elements and use filter(':input') as jQuery recommends on its documentation page. And here is the result (click on the image to see full image):

jquery selectors performance comparison
Comparison of different jQuery selectors

As you can see, the CSS attribute selector performs more than 800% better than :input!

The filter seems to be the worst solution. The reason probably is the universal selector * which is not performant. Anyway, we can use it in our plugin. Just put here to test to be sure.

The result is based on my OS (Windows 10) and browser (Firefox 54). You can run the test again and see the your result. I guess it’s not much different.

Conclusion

Sometimes a simple line of code can make your app faster in a way that you can imagine. This simple trick of using CSS attribute selector is not new. But it is useful for us in our specific situation. Have you ever met a similar problem? How did you resolve it? Please share with me in the comments.

1 Comment

Leave a Reply