Article originally posted on my personal website at How to optimize jQuery DOM manipulation
If you are working with JavaScript then most probably jQuery is a library you are using quite frequently. jQuery is useful and provides many features that are harder to achieve with basic JavaScript. Due to the fact that it usually runs on the client-side, many don’t pay too much attention to optimizing the code. That is why there are many websites that load slowly, have sluggish UIs, or seem to respond with delay. So, in this article, I will show an optimization technique that can save a lot of time when rendering the page with dynamically added elements.
Our scenario: Loading products without reloading the page
Let us take a look at a common use case where this technique may be useful. You are a developer that is working on an online store. Because of the nature of the infrastructure and the client’s requirements, React is not an option so you fall back to a more “traditional” approach. There is only one server application, being it Play (Java/Scala), CodeIgniter (PHP) or any other framework that using a template engine renders the DOM of the pages.
Now, as part of the catalog navigation feature, you get the partial result (in batches of 100 items) and display them on the page with a pagination menu at the bottom. When the next page is clicked, instead of physically going to a new page, you use AJAX calls to get the new items and do DOM manipulation to display them. The steps are like this:
- AJAX call to /items?page=
- Receive response as JSON
- Clear the existing displayed items from the page
- Rebuild the DOM with the new items
First implementation (bad): Rendering each element individually
Let us look at a portion of the code. This function creates the HTML for a product and alters the DOM so that it is displayed on the page (image, name, and price)
function makeItemOnPage(item, itemNo) {
// we create a container for the current item
var itemContainer = '<div class="col-sm-2 itemContainer" id="item-' + itemNo + '" style="padding: 10px"></div>';
$("#products").append(itemContainer);
// we create a div for the product imate and display it
var productImage = '<div class="productImage" id="productImage-' + itemNo + '"></div>';
var currentItemContainer = $("#products").find("#item-" + itemNo);
currentItemContainer.append(productImage);
$("#productImage-"+itemNo).append('<img src="' + item.image + '" />');
// We append the product name and the price
currentItemContainer.append('</div><div class="productDetails"><strong>' + item.name + '</strong> - ' + item.price + '$');
// We create an Add To Cart button
currentItemContainer.append('<button type="button" class="btn btn-success btn-block"><i class="fa fa-bell"></i> Add to cart</button>')
}
Let us render 1000 items in total and see the time it takes. I exaggerated a bit the number of items so that the total gains from optimization to be better shown. We can easily see how long it takes by using the browser’s performance analyzer. As can be seen in the image, it took about 1.7 seconds for the items to be rendered on the page. It may not seem much (we do have 1000 items), but the HTML in this case is quite simple and does not have too many inner objects. A page that has a much more complex design can easily have a more complex HTML code for each item. And even so, the user having to wait almost 2 seconds for the items to get displayed is not good from a UX point of view. I think we can optimize things quite a lot.
First thing we see is that we do a lot of search for elements in the page and many appends. We search for the items container, append a div for the current item container, search for it, append the image, append the name and price and after that another append for the button. Analyzing the times in the Performance Inspector, we see that those appends take quite a long time, almost equal to the total time. So, let’s try creating the HTML for the entire item as one string and appending it all once.
The code is like this:
function makeItemOnPage(item, itemNo) {
// we create a container for the current item
var productImageHtml = getProductImageHtml(item, itemNo);
var productDetailsHtml = getProductDetailsHtml(item, itemNo);
var addToCart = getAddToCartButton(item, itemNo);
var itemContainer = '<div class="col-sm-2 itemContainer" id="item-' + itemNo + '" style="padding: 10px">';
itemContainer += productImageHtml;
itemContainer += productDetailsHtml;
itemContainer += addToCart;
itemContainer += "</div>";
$("#products").append(itemContainer);
}
function getProductImageHtml(item, itemNo) {
return '<div class="productImage" id="productImage-' + itemNo + '"><img src="' + item.image + '" /></div>';
}
function getProductDetailsHtml(item, itemNo) {
return '<div class="productDetails"><strong>' + item.name + '</strong> - ' + item.price + '$</div>';
}
function getAddToCartButton(item, itemNo) {
return '<button type="button" class="btn btn-success btn-block"><i class="fa fa-bell"></i> Add to cart</button>';
}
Now, doing the benchmark again we clearly see a decrease in render time. It is now less than one second, about 1/3 of the previous time. This is because the number of calls to .append() was reduced to only one per item. But we can do even better.
Building the needed HTML and appending once
Now comes the final optimization. Instead of building each product view and appending it, we can do this by building the entire list of products and adding the resulting HTML to the container in one single go. This way we call append() once which will result in only one redraw of the UI elements. The code is almost identical, but instead of calling append at the end, we just return the resulting string.
function makeItems() {
$("#products").empty();
var items = getItems();
var itemNo = 0;
var items = "";
for (itemNo = 0; itemNo< items.length; itemNo++) {
items += makeItemOnPage(items[itemNo], itemNo);
}
$("#products").append(items);
}
function makeItemOnPage(item, itemNo) {
// we create a container for the current item
var productImageHtml = getProductImageHtml(item, itemNo);
var productDetailsHtml = getProductDetailsHtml(item, itemNo);
var addToCart = getAddToCartButton(item, itemNo);
var itemContainer = '<div class="col-sm-2 itemContainer" id="item-' + itemNo + '" style="padding: 10px">';
itemContainer += productImageHtml;
itemContainer += productDetailsHtml;
itemContainer += addToCart;
itemContainer += "</div>";
return itemContainer;
}
Now, where we receive our data from the server, after building the HML string, we call append on the container, similar to the code on the right. Let’s re-run the benchmark again.
Now we have less than 150ms in this particular example, more than 4 times faster than in the previous version and 12 times faster than the first version. Full source for this example can be downloaded at the original article on my site.
Conclusions
I used a similar technique to optimize page generation based on some input for an offline-only utility that runs in the browser. It was a log viewer and parser and the initial version took around 6 seconds to process a 3000 log file. After optimizing the calls, the same log was parsed and displayed in less than 0.8 seconds, a big improvement in both time and user experience.
Now, I know that generating HTML code like this has drawbacks, but there are many scenarios where not only does it help, it offers benefits like reduced server load. If you are careful to properly split code generation and don’t mix different elements in the same generator function, the JavaScript code can remain clean and easy to maintain.
As a final note, I am primarily a back-end developer, so more experienced JavaScript users may have even better solutions as well as objections to this approach.
Article originally posted on my personal website at How to optimize jQuery DOM manipulation
Top comments (0)