Monday, June 24, 2013

Templates: The unsolved mystery

This post is all about Templating (and HTML templating for JavaScript applications in particular) and how we really don't know how to do it yet. I'm sure there are a great many people out there that just go with the recommended templating engine for whatever tools they are using. But which should we really be using for out project? The truth is that we haven't found the best solution yet. So let's look at some of the soutions we have now.

Types of template engines

Render as a string

For the majority of templating solutions out there the output is a string. Why is this? Well there are two very good reasons

It's easier. You're HTML is written as a string so it's easy to just replace those placeholders and output that string

Thats how data is sent from the backend. So if your output is a string it can also be used on the backend to render your HTML templates.

The issue with this? Well on the frontend we still have to take that string and change it in to DOM nodes. This usually means we use innerHTML to convert the string and this means the browser then has to parse the string and create the nodes. If you look at your timeline in chrome developer tools you may see "parse HTML", and it can take a fair bit of time:

You can see in an unoptimized setting where you have lots of views come in that this may take a while. One way to get around this is to batch them all together before you do an innerHTML call. This will be a bit faster but is still slower than adding in a detached DOM tree.

Another big issue (perhaps the biggest issue) with this method is that you're creating a whole new DOM structure from the string. This means that any event listeners or state (such as current user focus or selection) will be lost. Backbone gets around this by having a top level element and using event delegation to handle events but this still does not address the issue of lost state, or even HTML that is added by subviews (there are plenty of plugins to help with the subview problem, you can lookup the backbone plugins wiki under Views or Frameworks).

Lightweight DOM representation

We all know the DOM is slow. However React (and I believe AngularJS) has taken a different approach where they keep their template will output their own representation of the DOM. This means they can compute the smallest amount of transforms (operations) they need to perform on the DOM to make changes. This is a good method if there are several changes happening to the DOM and means that we can still output a string but have our DOM exist on the page. However there is still a performance hit because we need to change the string to a DOM representation.

Output DOM

There isn't much out there that will give you straight DOM for a template. There is a project called HTMLBars which is promising and should eventually replace EmberJS's method of templating (which currently outputs a string that contains extra script tags that are used as markers for bound values). The plus side is that it's fast as what you're getting out of it is what you want to put in the page.

Clone DOM

Another way we could get a template is to clone a DOM structure and theoretically this should be even faster that outputting dom. This is the approach I outlined in my blog post about Optimizing often used templates and works well for Closure and PlastronJS as there may be operations between actually creating the DOM and putting it in the page so your data may have changed. This means that creating the template and filling it with data have to be two different operations whereas most traditional approaches will fill the template with data when it is created.

Speed

So knowing all this, let's see what the speed actually is like for a string vs dom based:

It's not the most scientific of results but you can see the test page code here.

Then for the string template I set it as an innerHTML and for the domTemplate I just run it. For the fastString test I added all the strings together before doing the innerHTML and for the binding I ran the domTemplate but also returned an object with functions that would change the values when needed. Times are in milliseconds and the number is the number of templates I generate

From this we can see that creating a pre-poulated DOM structure is certainly the way to go in terms of speed and even if we pass back functions that can handle the bindings it's still faster than using a string.

Data Binding

Data binding is important. It may be possible on simple sites to get away with re-rendering all your views but as soon as any meaningful user interaction is needed then you need to bind specific parts of your DOM to the data to keep the user state when data changes. Most libraries handle it natively and for those that don't you can try Rivets.js. To try and use this at work I wrote a handlebars to rivets converter.

Converting Handlebars to Rivets

I've since abandoned the project but it's interesting to look at what is needed in conversion because it gives some insight in to the differences between outputting DOM and outputting a string. For those that don't know how rivets works it looks at data attributes on nodes and then binds to them. This means you get conversions like this:

The trickiest though were if statments that contained DOM nodes under them. There is no real "if" statement in rivets and the whole point of changing to the DOM was so that we wouldn't have to recreate new DOM nodes when there is a change in the model. To get around this the nodes under an if statement used the data-show attribute so they would be hidden when it evaluated to false. It actually worked rather well but there are three reasons why I'm abandoning it

There are better data binding tools out there - i.e. AngularJS

Rivets.js has computed properties which are essential to the converter but they don't take the values of the bound properties as arguments, instead you have to have those properties provided to the function separately through a closure which stop the functions being reused and makes it impossible to convert when the scope changes (say inside an each loop). To get around this I patched a version of rivetsjs that is included in the github repo

It was slow. Creating the DOM was fast as we only needed to clone a static DOM structure but then that structure had to be parsed to setup the bindings and those bindings run to populate the data. This made it slow at startup.

Logic

A big issue I take with templates is the amount of logic that is put in to them. You can read more of my thoughts in my Logic in templates post. This is a major reason handlebars is slower than other engines, because you can do so much with it. One of the things I wanted to do was reduce the logic in our templates and so I created Backbone.ModelMorph. The idea being that we create a new model from what we already have using a schema that holds our computed properties but still acts as a model sending out changes. This means we can take out the logic from the actual template and put it in a "presentation model" that sits between the template and the actual data model. This means we've separated all our concerns with the structure in the template, the data in the actual model and the presentation of the data in the ModelMorph.

So what's next?

Next up I'm going to be taking the guts from my handlebars converter and seeing if I can't make a simplified HTMLBars that will have less logic (say only one IF statement or EACH depth allowed) that can output a DOM stucture but also return an object that will allow you to hook up bindings. Only allowing one level should still give flexibility yet force people to think about how much logic they're putting in while making it much easier to write for me and also means that I won't have to deal with intricate layers of scope so the final function should also perform well.

So why am I calling it the unsolved mystery? Well as far as I know the correct way hasn't been found yet even though I have an idea of what I'd like. What will be interesting is to see how well it all fits in with the new <template> tag and web components.