How to improve rendering performance when using $.html()

Create a new DocumentFragment to pre-render all the items, then update the DOM once.

Also, favor this.$(...) over the global jQuery selector $(...).

this.$ is a proxy to this.$el.find(...) which is more efficient, and less prone to select something outside of the view.

Using jQuery’s core function ($()) inside a view can fail if the view wasn’t rendered yet. So it’s better to always manipulate through this.$el so you can make changes even before the view is actually put in the DOM.

Keep all the sub views created in an array to cleanly remove them later.

initialize: function() {
    this.childViews = [];
},
render: function() {
    // cache the list jQuery object
    this.$list = this.$("#item-table");

    // Make sure to destroy every child view explicitely 
    // to avoid memory leaks
    this.cleanup();

    this.renderCollection();
    return this;
},

The real optimization starts here, with a temporary container.

renderCollection: function() {
    var container = document.createDocumentFragment();

    this.collection.each(function(model) {
        // this appends to a in memory document
        container.appendChild(this.renderItem(model, false).el);
    }, this);

    // Update the DOM only once every child view was rendered.
    this.$list.html(container);
    return this;
},

Our renderItem function can still be used to render a single item view and immediatly put it in the DOM. But it also provides an option to postpone the DOM manipulation and it just returns the view.

renderItem: function(model, render) {
    var view = new Item({ model: model });

    this.childViews.push(view);
    view.render();
    if (render !== false) this.$list.append(view.el);
    return view;
},

To avoid memory leaks with dangling listeners, it’s important to call remove on each view before forgetting about it.

I use an additional optimization by deferring the actual call to remove so we don’t waste time now while the user waits.

cleanup: function() {
    var _childViewsDump = [].concat(this.childViews);
    this.childViews = [];

    while (_childViewsDump.length > 0) {
        var currentView = _childViewsDump.shift();
        // defer the removal as it's less important for now than rendering.
        _.defer(currentView.remove.bind(currentView), options);
    }
}

Leave a Comment