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

I am working on a Backbone demo app that shows a list of tweets. As I am replacing all the "tweets" with different data I clear the list using $.html()
render: function() {
$("#item-table").html('');
this.collection.each(this.addItem);
}
I was wondering if anyone could give me a hint with what can I replace this $.html() for better performance, because by using $.html() I am causing reflows and which gives bad layout process times.
There are two other places in the code where I use $.html() and it would be really great if someone could give me advice on how to change those too if those other places are even possible.

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);
}
}

Related

Events not firing after reinitialising backbone views

I'm trying to tidy code in my Backbone project. One of the problems I face is that I initialise all my views in the render's initialise function. I've now decided to only initialise a single view (and it's children) at a time.
The rendering of the pages works and I can swap backward and forward between views. Events that are bound in the view fire after a hard refresh of the page (F5 etc) but once I've moved to another view, the events no longer fire. I don't understand why as the previous view should be totally removed on second initialisation. We should then get a clean render, just as it would be on first load after a refresh. Can anyone explain why the events aren't firing?
Below are code examples to demonstrate the problem:
newView: function(view){
//Check if the holding variable is defined. If it is then
//call the close function
if (routerView == undefined){
console.log("routerview is undefined")
} else {
// This calls a function on the view which will remove any
//children, once it's done that it will remove its self.
routerView.close();
}
// When removing the view it removes the parent element in the index file.
// Here we add the container back in and set it to the new view's el
if ( $('#AppContainer').length == 0 ){
// Add new div container to the app and assign it to the 'el'
view.el = $('body').prepend('<div id="AppContainer"></div>');
}
// Return view to the route for rendering.
return routerView;
},
The close function inside one of the views would look something like this:
close: function(){
//Deal with any child views here as well.
this.remove();
},
Finally, in the route where we'd call the newView function would look
admin: function(){
// Call the newView function with a new instance of the AdminView and then assign it back
this.adminView = router.newView( new AdminView({ el : $("#AppContainer")} ));
//Render the view
this.adminView.render();
},
I have done some more work investigating the problem and I've discovered it. The problem was two fold but appeared on the same line of code.
view.el = $('body').prepend('<div id="AppContainer"></div>');
I discovered on the backbone docs that you should use the setElement function to alter a view's element. This then transfers all bound events which now means they work.
I then discovered that $('body').prepend('<div id="AppContainer"></div>') would return a reference to body and not the new #AppContainer but it actually returns a reference to the body which meant that the content of view was being placed in the body.

Backbone.View: delegateEvents not re-binding events to subview

I have broken down this issue into the smallest example possible (i.e., it's only to demonstrate the problem, not to necessarily represent a real-world scenario).
Let's say I have a parent view ("MainView" here) that contains a child view ("SubView" here). If, at any point, I need to re-render the parent view (which thereby re-renders the child view), I am losing the event bindings in the child view, despite calling delegateEvents.
A jsfiddle can be found here: http://jsfiddle.net/ya87u/1/
Here is the code in full:
var MainView = Backbone.View.extend({
tagName : "div",
initialize : function() {
this.subView = new SubView();
},
render : function() {
this.$el.html(this.subView.render().$el); // .html() breaks binds
this.subView.delegateEvents(); // this re-establishes them
return this;
}
});
var SubView = Backbone.View.extend({
tagName : "div",
events : {
"click .button1" : "onButtonPress"
},
onButtonPress : function() {
alert("Button pressed");
},
render : function() {
var html = "<button class='button1'>Button1</button>";
this.$el.html(html);
return this;
}
});
var v = new MainView();
$("#area1").html(v.render().$el); // binds work fine
// do some work... then re-render...
$("#area1").html(v.render().$el); // breaks event binds
I do not understand why this is happening. Any input would be greatly appreciated.
To preserve the bindings you could do three things.
Use $.append instead of $.html
Using $.append will keep your bindings intact and also take care that you don't lose any sibling content.
Just re-render without re-attaching
In your code you re-render your view and also re-attach it's content. This is not necessary. Simply calling v.render() will achieve the same visual result but keep your bindings intact.
var v = new MainView();
$("#area1").html(v.render().$el); // binds work fine
// do some work... then re-render…
v.render(); // keeps event binds
$("#area1").append(v.render().$el); // works too
Use $.empty() before calling $.html()
According to the jQuery documentation for $.html(htmlString)
If you are keeping references to these DOM elements and need them to be unchanged, use .empty().html( string ) instead of .html(string) so that the elements are removed from the document before the new string is assigned to the element.
The important part is that $.html expects an html string, not elements and also unbinds any event handlers present on child nodes. The only way I can explain this behavior is that somehow through transformation of the dom tree to a string and vice versa the events get removed. Interestingly enough, using $.empty() as mentioned in the documentation solves the problem.
// any $.html reference needs to have an $.empty too
this.$el.empty().html(this.subView.render().$el);
// and then later for your mainView
$("#area1").empty().html(v.render().$el);

Backbone still leaking memory after several attempts at cleaning

I am having issues at solving backbone memory leaks, so far I attempted to clean the views using jquery CleanData when the views are removing from the DOM.
Each time a view is removed (even via jQuery .html()) it hit the dispose() method that should theoretically kill all references that stop the view from being collected. Unfortunately the app just build up memory
code below,
Backbone.View.prototype.dispose = function(){
// unbind all events, so we don't have references to child views
this.unbind();
this.off();
// if we have a collection - unbind all events bound to this context
this.collection && this.collection.off(null, null, this);
// do the same with a model
this.model && this.model.off(null, null, this);
delete this;
};
clean data :
$.cleanData = function( elems ) {
$.each( elems, function( i, elem ) {
if ( elem ) {
$(elem).trigger("dispose");
}
});
oldClean(elems);
};
The code work, dispose is hit (I added the event in the view) but it's never collected anyway when I change page.
(About the dispose event..)
I do not explicitly call remove on all views. the app container is emptied, jQuery does cleanData. I added an event dispose & I trigger that func in cleandata
I think the problem is delete this doesn't do what you thought it would. It depends on how you initiate your view. Do you assign your view to any variable or initiate it in a scope that live beyond the point where you change your page?
Also, there is a function remove() on Backbone View
More on JavaScript delete http://perfectionkills.com/understanding-delete/ and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete

Why this render function doesn't work?

I have been playing with backbone and trying to learn it. I'm stuck at this point for a while. Not able to figure out what's wrong with following code?
render: function() {
this.$el.empty();
// render each subview, appending to our root element
_.each(this._views, function(sub_view) {
this.$el.append(sub_view.render().el); // Error on this line
});
You have got context issue. this you are refering to doesn't contain the $el you are looking for. You can fix this by declaring a self variable that points to appropriate this. Following code should work for you.
render: function() {
var self = this; //Added this line to declare variable (self) that point to 'this'
this.$el.empty();
_.each(this._views, function(sub_view) {
self.$el.append(sub_view.render().el); //Used 'self' here instead 'this'
});
Side Note: As you are leaning backbone also you should know about a very commong JavaScript problem with document reflow. You are render a view for every single model in the collection. It can lead to performance issues and especially on old computers and mobile devices. You can optimise your code by rendering everything in container and appending it once, rather than updating DOM each time. Here is an example:
render: function() {
this.$el.empty();
var container = document.createDocumentFragment();
_.each(this._views, function(sub_view) {
container.appendChild(sub_view.render().el)
});
this.$el.append(container);
}

Click binding in jQuery with backbone

I'm using Backbone.js, and in one of my main views I've encountered a very strange bug that I can't for the life of me figure out how to solve.
The view looks a look like the new Twitter layout. It receives an array of objects, each of which describes a collection and views elements that act on that collection. Each collection is represented by one tab in the view. The render() method on my view takes this array of collection objects, clears out the tabContainer DOM element if it isn't already empty, renders the tabs and then binds events to each of those tabs.
Now in my code I have the method to render the tabs and the method to bind the click handlers to those tabs sequentially. This works fine the first time I execute render(), but on subsequent calls of render(), the click handlers are not bound. Here's the relevant code snippet:
initialize: function() {
// Context on render(), _addAllTabs and _bindTabEvents is set correctly to 'this'
_.bindAll(this, 'render', 'openModel', 'closeModel', 'isOpen', 'addAllModels', '_switchTab',
'addOneModel', '_addTab', '_removeTab', '_addAllTabs', '_loadCollection',
'_renderControls', '_setCurrentCollection', '_loadModels', '_bindTabEvents');
this.template = JST['ui/viewer'];
$(this.el).html(this.template({}));
// The tabContainer is cached and always available
this.tabContainer = this.$("ul.tabs");
this.collectionContainer = this.$("#collection_container");
this.controlsContainer = this.$("#controls");
this.showMoreButton = this.$("#show_more_button");
},
render: function(collections, dashboard) {
// If _bindTabEvents has been called before, then this.tab exists. I
// intentionally destroy this.tabs and all previously bound click handlers.
if (this.tabs) this.tabContainer.html("");
if (collections) this.collections = collections;
if (dashboard) this.$("#dashboard").html(dashboard.render().el);
// _addAllTabs redraws each of the tabs in my view from scratch using _addTab
this._addAllTabs();
// All tabs *are* present in the DOM before my _bindTabEvents function is called
// However these events are only bound on the first render and not subsequent renders
this._bindTabEvents();
var first_tab = this.collections[0].id;
this.openTab(first_tab);
return this;
},
openTab: function (collectionId, e) {
// If I move _bindTabEvents to here, (per my more thorough explanation below)
// my bug is somehow magically fixed. This makes no friggin sense.
if (this.isTabOpen(collectionId)) return false;
this._switchTab(collectionId, e);
},
_addAllTabs: function() {
_.each(this.collections, this._addTab );
},
_bindTabEvents: function() {
this.tabs = _.reduce(_.pluck(this.collections, "id"), _.bind(function (tabEvents, collectionId) {
var tabId = this.$("#" + collectionId + "_tab");
tabEvents[collectionId] = tabId.click(_.bind(this._switchTab, this, collectionId));
return tabEvents
}, this), {});
},
_addTab: function(content) {
this.tabContainer.append(
$('<li/>')
.attr("id", content.id + "_tab")
.addClass("tab")
.append($('<span/>')
.addClass('label')
.text(content.name)));
//this._loadCollection(content.id);
this.bind("tab:" + content.id, this._loadCollection);
pg.account.bind("change:location", this._loadCollection); // TODO: Should this be here?
},
etc..
As I said, the render() method here does work, but only the first time around. The strange part is that if I move the line this._bindTabEvents(); and make it the first line of the openTab() method like in the following snippet, then the whole thing works perfectly:
openTab: function (collectionId, e) {
this._bindTabEvents();
if (this.isTabOpen(collectionId)) return false;
this._switchTab(collectionId, e);
},
Of course, that line of code has no business being in that method, but it does make the whole thing work fine, which leads me to ask why it works there, but doesn't work sequentially like so:
this._addAllTabs();
this._bindTabEvents();
This makes no sense to me since, it also doesn't work if I put it after this line:
var first_tab = this.collections[0].id;
even though that is essentially the same as what does work insofar as execution order is concerned.
Does anyone have any idea what I'm doing wrong and what I should be doing to make this correct (in terms of both behavior and coding style)?
In your view's render function, return this.delegateEvents(); I think you are losing your event bindings across your renderings and you need to re-establish them.
See this link for the backbone.js documentation for that function:
backbone.js - delegateEvents
When you switch tabs you are not simply showing/hiding content you are destroying and rebuild dom element so you are also destroying event liseners attached to them. that is why the events only work once and why adding _bindTabEvents into render works, because you are re-attaching the events each time.
when this line executes : this.tabContainer.html(""); poof... no more tabs and no more tab events.

Categories