The basic philosophy of jQuery is to start with some existing HTML sent over vanilla HTTP by the server. That HTML should be all you need (so that people without a JavaScript-enabled browser can still use the web site). Then, jQuery enhances that HTML by adding new behavior (usually changing the properties of existing elements, sometimes adding new elements).
This is very useful for small pieces of behavior, but writing complete and complex components is hard for several reasons:
- A partial view strategy is required on the server side to insert the appropriate HTML in the appropriate location (as opposed to leaving an empty hole and having the component generate its own HTML).
- If the behavior of your component is complex, then there will be a lot of parsing going on. A typical example would be sorting a table by a “date” column—since the date format in itself cannot be parsed (culture-dependent and may contain “Yesterday”, “13 seconds ago” and similar shortcuts).
- Sometimes, the server needs to add information that is not visible, but is needed by the JavaScript. The format for sending this data (attribute, hidden field…) is difficult to document and type-check.
- Selecting precisely the right fields in a blob of HTML, without hitting any others, is hard, especially for components that may later contain sub-components. Class-based selection is slow, id-based selection involves heavy logistics to move the identifiers around, and complete traversal takes a while and breaks if the HTML changes.
My preferred approach to JavaScript components is to receive JSON-formatted data from the server (easy to parse) from which I construct the DOM elements I need and capture them at the same time.
var $comment = $('<div><img/><span/><div/></div>')
.addClass("comment");
var obj =
{
$self : $comment,
$img : $comment.children('img')
.attr('src',data.imgUrl),
$name : $comment.children('span')
.text(data.authorName)
.addClass('authorName'),
$body : $comment.children('div')
};
$.each(data.text,function(k,t){
$('<p/>').text(t).appendTo(obj.$body);
});
return obj;
The point is that you then have access, through the returned object, to all the relevant elements within the comment, so that you may target them with effects without any risky selector-based magic. Besides, if the HTML format of comments changes, you will only have to change the code above and nothing else.
And of course, using text() escapes any dangerous HTML you might have.
To make the above appear in your code, all you have to do is:
var $commentsList = $('#my-comments-list');
$.each (comments, function(i,c){
var obj = $comments[i] = renderComment(c);
obj.$self.appendTo($commentsList);
});
This is usually where you hit a performance wall, because this is one of the slowest ways of using jQuery on a web page.
I’ve been in this situation recently on a smallish website that basically displays a list of contacts invited to various events as a 10-column/300-row table that includes additional functionality such as:
- Dynamically add or remove new rows (with server-side confirms)
- Rows are grouped together, and groups can be collapsed and expanded
- Clicking on rows opens a modal editor, modifications are propagated back to the table
- The data and formatting for certain rows depend on some other rows
The initial approach was exactly as described above: every cell was constructed as $('<td/>'), classes and attributes were applied to it, then all cells were inserted into rows constructed as $('<tr/>'), and these in turn were appended to the table tbody. Since some parts of the table were clickable to achieve various effects, jQuery’s click() function was used to add the appropriate event handlers, and the event handlers were closures that contained all relevant information about what row had to be collapsed or what element had to be removed.
The average time for rendering all of this was a solid 2200ms on Firefox 3.5, which felt about as dynamic as a dead tortoise nailed to a slab of concrete. For comparison purposes, rendering the data server-side and sending it to the client took about 390ms on average (arguably, the server would have scaling issues as it would have to render the HTML for all clients, but still).
2200ms means about 7ms per row. The problem here isn’t that the jQuery code is slow, but rather that it’s executed so many times to add up to a pretty large number.
My first attempt to improve performance was to avoid constructing rows cell by cell, instead building the final HTML of the row in one shot and then selecting clickable elements inside the row through their class to apply event handlers. Rows were then inserted into the table body using jQuery’s DOM functions. The new rendering time was 1800ms, which was not as good as I hoped my improvement to be.
The second step was to move away from selecting clickable elements to apply event handlers. This meant that I could either insert the event handler code in the HTML (but this meant no closures, so I would have to rely on global, non-garbage-collected behavior) or add a click event to the entire table and determine what element had been clicked (and parsing the DOM for information about what to do with the click, which was annoying).
I went with the first way, rewriting my code as global handlers and eliminating all the select-child-with-class overhead. Rows were still constructed independently and inserted independently. The improvement was sensible, as the rendering time was then 980ms.
The last wave of optimizations consisted in making sure the HTML for the entire table body was generated in one shot and concatenated as an array (using [a,b,c].join('') instead of a+b+c). This creates 5223-element array, concatenated into a string containing 72357 characters, which is then inserted into the table body using jQuery’s html() function. The entire process, including preliminary processing of the data to be displayed, takes about 160m (a 13.7× performance increase).
The change was mostly moving from this design pattern:
function renderRow(data)
{
$tr = $('<tr/>');
$('<td/>')
.addClass('name')
.append($('<a/>')
.text(data.name)
.click(function(){ frobnicate(data.id); }))
.appendTo($tr);
// ...
return $tr;
}
To this one:
function renderRow(data,html)
{
html.push(
'<tr><td>',
'<a href="javascript:frobnicate(',
data.id,
')">',
esc(data.name),
'</a></td>',
// ...
'</tr>'
);
}
Again, this is an extreme situation where page-generation goes way out of hand because a lot of rows are generate—the net benefit, as far as rendering a single row is concerned, is around 6ms. If your page contains only a small number of complex components, you can ignore the performance issues to get the components done, and only optimize if it turns out to be noticeable.
Recent Comments