Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Javascript Templates Comparison: Hogan, dust, doT, underscore

JavaScript Templates Engines

A quick comparison/ benchmark between Hogan, Dust, doT and underscore

Presentation and Readability

Hogan.js

Developed by Twitter (same team as Bootstrap), use exactly the same syntax as Mustache, but more performant and more stuff available server side.

<h1>My name is {{ name }}</h1>
{{#i18n}}I saw {{ animals.otaries }} otaries and {{ animals.wapitis }} wapitis{{/i18n}}
{{#song}}
    <div>{{ . }}<div>
{{/song}}
dust.js

Developed by LinkedIn as their main mobile front-end template engine. Really powerful, much more patterns available.

<h1>My name is {name}</h1>
{@i18n key="I saw {animals.otaries} otaries and {animals.wapitis} wapitis" /}
{#song}
    <div>{.}</div>
{/song}
doT.js

Designed to be the fastest JS engine ever

<h1>My name is {{= it.name }}</h1>
{{= it.i18n('I saw %s otaries and %s wapitis', it.animals.otaries, it.animals.wapitis) }}
{{~ it.song :value:index }}
    <div>{{= value }}</div>
{{~}}
underscore.js

Templating is a part of the underscore.js utility library developed by Backbone Author, @jaskhenas. Goal is to mimic erb templates style in JavaScript

<h1>My name is <%= name %></h1>
<%= i18n('I saw %s otaries and %s wapitis', animals.otaries, animals.wapitis) %>
<% _.forEach(song, function(value, index) { %>
    <div><%= value %><div>
<% }) %>
underscore without with

without the with statement: to enable the direct use of object properties without prefix ({{ name }} and not {{ myOb.name }}), underscore compile the template function inside a with statement, setting the function scope within the data object. It's easier to read, but really known to have bad performances. To avoid that, you can define custom context object and pass it as the 3rd argument of _.template, ex: _.template('Hello {{ data.name }}', null, { variable: 'data' }).
@see underscore.js#template

Subjectively
  • I think Hogan has the most readable code. Very easy to guess, functions are expressed as blocks, curly braces syntax inherited from mustache is know now and easy to read
  • dust is quite readable, but once you want custom functions (and complex stuff), it's a completely own language, that needs a real learning curve
  • doT begins to be very complex when doing complex stuff (array rendering is not that easy to guess)
  • underscore is almost literal JavaScript inside, which can be cool, but it's not really what we want from a template engine

Performance (compilation + rendering)

For 1000 rendering iterations:

Lib compiling time Avg. rendering compiled size
Hogan 2 ms 0.022 ms 513 chars.
Dust 15 ms 0.019 ms 582 chars.
doT 1 ms 0.007 ms 307 chars
_ 1 ms 0.014 ms 425 chars.
_ (no with) 1 ms 0.009 ms 421 chars.

A real outbust from doT, while underscore seemed really performant too. But we're currently talking about microseconds, so it may not be really relevant unless you're developing a videogame, or an app with a lot of loops and on-the-fly rendering.

microtime = require 'microtime'
hogan = require 'hogan.js'
dust = require 'dustjs-linkedin'
dot = require 'dot'
_ = require 'underscore'
## ------------------------------
## INIT
## ------------------------------
data =
name: 'Arne Vinzon'
animals:
otaries: 12
wapitis: 16
song: [
'Les otaries'
'Les otaries'
'Les otaries'
'Du zoo de Vincennes'
]
ngettext = (str, args...) ->
(matches = str.match(/%s/g)) and matches.forEach (item, index) ->
str = str.replace /%s/, args[index]
return str
## ------------------------------
## TEMPLATES ENGINES
## ------------------------------
ENGINES = {}
ENGINES['hogan'] =
tmpl: '''
<h1>My name is {{ name }}</h1>
{{#i18n}}I saw {{ animals.otaries }} otaries and {{ animals.wapitis }} wapitis{{/i18n}}
{{#song}}
<div>{{ . }}<div>
{{/song}}
'''
init: (@data) ->
@data.i18n = (text) ->
searchReg = /\{\{\s?([^}\s]+)\s?\}\}/g
replaceReg = /\{\{ ?| ?\}\}/g
matches = text.match(searchReg)
replaces = []
matches.forEach (item, index, ar) =>
replaces[index] = @[item.replace(replaceReg, "")]
text.replace searchReg, '%s'
replaces.forEach (item, index, ar) =>
text = text.replace '%s', item
return text
compile: () ->
if not @_compileFn?
@_compileFn = hogan.compile @tmpl
return @_compileFn
render: (cb) ->
return @compile().render @data
getSource: () ->
return hogan.compile @tmpl, { asString : true }
ENGINES['dust.js'] =
tmpl: '''
<h1>My name is {name}</h1>
{@i18n key="I saw {animals.otaries} otaries and {animals.wapitis} wapitis" /}
{#song}
<div>{.}</div>
{/song}
'''
init: (@data) ->
renderParameter = (name, chunk, context, bodies, params) ->
if params and pName = params[name]
if typeof pName is 'function'
output = ""
chunk.tap((data) ->
output += data
return ""
).render(pName, context).untap()
return output
else
return pName
return ""
dust.helpers["i18n"] = (chunk, context, bodies, params) ->
key = renderParameter 'key', chunk, context, bodies, params
return chunk.write key
compile: () ->
if not @_compileFn?
@_compileFn = dust.compile @tmpl, 'dust'
dust.loadSource @_compileFn
return @_compileFn
renderAsync: yes
render: (cb) ->
dust.render 'dust', data, (err, out) ->
if err then console.log 'ERR ' + err
cb?(out)
getSource: () ->
return @_compileFn.toString()
ENGINES['doT.js'] =
tmpl: '''
<h1>My name is {{= it.name }}</h1>
{{= it.i18n('I saw %s otaries and %s wapitis', it.animals.otaries, it.animals.wapitis) }}
{{~ it.song :value:index }}
<div>{{= value }}</div>
{{~}}
'''
init: (@data) ->
@data.i18n = ngettext
compile: () ->
if not @_compileFn?
@_compileFn = dot.compile @tmpl
return @_compileFn
render: (cb) ->
return @compile()(@data)
getSource: () ->
return @compile().toString()
ENGINES['underscore.js'] =
tmpl: '''
<h1>My name is <%= name %></h1>
<%= i18n('I saw %s otaries and %s wapitis', animals.otaries, animals.wapitis) %>
<% _.forEach(song, function(value, index) { %>
<div><%= value %><div>
<% }) %>
'''
init: (@data) ->
@data.i18n = ngettext
compile: () ->
if not @_compileFn?
@_compileFn = _.template @tmpl
return @_compileFn
render: (cb) ->
return @compile()(@data)
getSource: () ->
return @compile().source
ENGINES['underscore.js (without `with`)'] =
tmpl: '''
<h1>My name is <%= it.name %></h1>
<%= _.i18n('I saw %s otaries and %s wapitis', it.animals.otaries, it.animals.wapitis) %>
<% _.forEach(it.song, function(value, index) { %>
<div><%= value %><div>
<% }) %>
'''
init: (@data) ->
_.i18n = ngettext
compile: () ->
if not @_compileFn?
@_compileFn = _.template @tmpl, null, { variable: 'it' }
return @_compileFn
render: (cb) ->
return @compile()(@data)
getSource: () ->
return @compile().source
## ------------------------------
## RUN THIS HELL!
## ------------------------------
LOOPSIZE = 1000
for name, ng of ENGINES
do (name, ng) ->
console.log "----- #{name} -----"
ng.init data
console.time 'compilation'
ng.compile()
console.timeEnd 'compilation'
console.log 'Pre-compiling size:', ng.getSource().length
iterations = LOOPSIZE
count = 0
add = (start) ->
count += (microtime.now() - start)
while iterations--
start = microtime.now()
ng.render()
count += (microtime.now() - start)
renderingTime = (count / LOOPSIZE) / 1000
console.log 'Avg. rendering time:', renderingTime.toFixed(4), 'ms'
# console.log ''
# console.log '$ compiled fn:\n' + ng.getSource() + '\n'
# console.log '$ result:\n' + ng.render()
console.log ''
----- hogan -----
compilation: 2ms
Pre-compiling size: 513
Avg. rendering time: 0.0210 ms
----- dust.js -----
compilation: 21ms
Pre-compiling size: 582
Avg. rendering time: 0.0142 ms
----- doT.js -----
compilation: 1ms
Pre-compiling size: 307
Avg. rendering time: 0.0083 ms
----- underscore.js -----
compilation: 1ms
Pre-compiling size: 425
Avg. rendering time: 0.0144 ms
----- underscore.js (without `with`) -----
compilation: 0ms
Pre-compiling size: 421
Avg. rendering time: 0.0111 ms

dharFr commented Oct 4, 2013

Any chance you can adapt your test to http://jsperf.com? ^^

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment