Mustache JS engine rumble: Mustache.js vs Handlebars.js vs Hogan.js vs templayed.js vs LT

JavaScript performance comparison

Revision 31 of this test case created by

Preparation code

<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/0.7.2/mustache.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/1.0.0/handlebars.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/hogan.js/2.0.0/hogan.js"></script>
<script src="https://archan937.github.io/templayed.js/templayed.js"></script>
<script>
/*!
 * LT - Little Template engine of {{mustache}}
 * https://github.com/rhyzx/lt
 */
!(function (root, undefined) {
    var _isArray = Array.isArray || function(obj) {
        return Object.prototype.toString.call(obj) === '[object Array]'
    }

    // print value
    function print(value, escape) {
        return typeof value === 'undefined'
             ? '' // placeholder
             : ( escape && /[&"<>]/.test(value += '') )
             // escape HTML chars http://www.w3.org/TR/html4/charset.html#h-5.3.2
             ? value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
             : value
    }

    // get defined value from context stack
    function get(scope, depth) {
        scope = scope.replace(/^(\.\.\/)+/, function (all, one) {
            depth -= all.length / one.length // parent path
            return ''
        })
        if (depth < 0) return "undefined"
        if (scope === '.') return "s" +depth // this
        var first = scope.match(/^[^.]+/)[0]  // nest path support, extract first scope for context finding
        var code = ''
        while (depth > 0) code += "typeof s" +depth +"." +first +" !== 'undefined' ? s" +depth-- +"." +scope +" : "
        return code +"s0." +scope
    }

    // core
    function compile(source) {
        var inverted = 0, depth = 0 // context stack depth
        var compiled = new Function("s0", "print", "_isArray", "var out = '" +source
        .replace(/\\/g, "\\\\") // escape \
        .replace(/'/g, "\\'")   // escape '
        .replace(/\{\{([\^#/!&]?)([^{\n]+?)\}\}/g, function (a, flag, scope) { // block
            switch (flag) {
            case '^':   // if not
                inverted++
                return "'; var value = " +get(scope, depth)
                     + " ; if (!value || (_isArray(value) && value.length === 0)) { out += '"
            case '#':   // if/each/TODO lambdas/TODO helper
                return "'; var value = " +get(scope, depth++)
                     + " ; var list = value ? _isArray(value) ? value : [value] : []"
                     + " ; for (var i=0, len=list.length; i<len; i++) {"
                     + " ; var s" +depth +" = list[i]; out += '"
            case '/':   // close
                inverted > 0 ? inverted-- : depth--
                return "'} out += '"
            case '!':   // comments
                return ""
            //case '>':   // TODO partials
            case '&':   // print noescape
                return "' +print(" +get(scope, depth) +", false) +'"
            default :   // print escape
                return "' +print(" +get(scope, depth) +", true) +'"
            }
        })
        .replace(/\n/g, "\\n") // escape cr
        +"'; return out")

        var template = function (data) {
            return compiled(data, print, _isArray)
        }
        return template.render = template // render api
    }
    compile.compile = compile // compile api

    // exports
    if (typeof module !== 'undefined' && module.exports) {
        module.exports = compile // CommonJS
    } else if (typeof define === 'function' && define.amd) {
        define('lt', compile)   // AMD
    } else {
        root['lt'] = compile    // <script>
    }
})(this)
</script>

      
<script>
Benchmark.prototype.setup = function() {
  var tests = [{
    template: "<p>My name is {{name}}!</p>",
    variables: {
      name: "Paul Engel"
    }
  }, {
    template: "<p>My name is {{name}}!{{!name}}</p>",
    variables: {
      name: "Paul Engel"
    }
  }, {
    template: "<p>{{html}} {{&html}}</p>",
    variables: {
      html: "<strong>Paul Engel</strong>"
    }
  }, {
    template: "<p>{{html}} {{html}}</p>",
    variables: {
      html: "<strong>Paul Engel</strong>"
    }
  }, {
    template: "<p>This is shown!{{#show}} Psst, this is never shown{{/show}}</p>",
    variables: {}
  }, {
    template: "<p>This is shown!{{#show}} Psst, this is never shown{{/show}}</p>",
    variables: {
      show: false
    }
  }, {
    template: "<p>This is shown!{{#shown}} And, this is also shown{{/shown}}</p>",
    variables: {
      shown: true
    }
  }, {
    template: "<p>My name is {{person.first_name}} {{person.last_name}}!</p>",
    variables: {
      person: {
        first_name: "Paul",
        last_name: "Engel"
      }
    }
  }, {
    template: "{{name}}<ul>{{#names}}<li>{{name}}</li>{{/names}}</ul>{{^names}}Sorry, no people to list!{{/names}}",
    variables: {
      names: []
    }
  }, {
    template: "<p>{{name}}</p><ul>{{#names}}<li>{{name}}</li>{{/names}}</ul>{{^names}}Sorry, no people to list!{{/names}}<p>{{name}}</p>",
    variables: {
      name: "Chunk Norris",
      names: [{
        name: "Paul"
      }, {
        name: "Engel"
      }]
    }
  }, {
    template: "<ul>{{#names}}<li>{{.}}{{foo}}</li>{{/names}}</ul>",
    variables: {
      names: ["Paul", "Engel"]
    }
  }, {
    template: "<ul>{{#names}}<li>{{fullName}}</li>{{/names}}</ul>",
    variables: {
      names: [{
        firstName: "Paul",
        lastName: "Engel"
      }, {
        firstName: "Chunk",
        lastName: "Norris"
      }]
      // remove lambdas for LT don't support yet(stringify function can consume lots)
    }
  }];
  var compiled = {
    "Mustache.js": [],
    "Handlebars.js": [],
    "Hogan.js": [],
    "templayed.js": [],
    "lt": []
  };
  for (var i = 0; i < tests.length; i++) {
    var template = tests[i].template;
    compiled["Mustache.js"].push(Mustache.compile(template.replace(/\.\.\//g, "")));
    var HandlebarsT = Handlebars.compile(template);
    HandlebarsT({}); //disable Handlebars' lazy-compile
    compiled["Handlebars.js"].push(HandlebarsT);
    compiled["Hogan.js"].push(Hogan.compile(template.replace(/\.\.\//g, "")));
    compiled["templayed.js"].push(templayed(template));
    compiled["lt"].push(lt.compile(template));
  }

};
</script>

Preparation code output

<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/1.0.0/handlebars.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/hogan.js/2.0.0/hogan.js"></script> <script src="https://archan937.github.io/templayed.js/templayed.js"></script> <script> /*! * LT - Little Template engine of {{mustache}} * https://github.com/rhyzx/lt */ !(function (root, undefined) { var _isArray = Array.isArray || function(obj) { return Object.prototype.toString.call(obj) === '[object Array]' } // print value function print(value, escape) { return typeof value === 'undefined' ? '' // placeholder : ( escape && /[&"<>]/.test(value += '') ) // escape HTML chars http://www.w3.org/TR/html4/charset.html#h-5.3.2 ? value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;') : value } // get defined value from context stack function get(scope, depth) { scope = scope.replace(/^(\.\.\/)+/, function (all, one) { depth -= all.length / one.length // parent path return '' }) if (depth < 0) return "undefined" if (scope === '.') return "s" +depth // this var first = scope.match(/^[^.]+/)[0] // nest path support, extract first scope for context finding var code = '' while (depth > 0) code += "typeof s" +depth +"." +first +" !== 'undefined' ? s" +depth-- +"." +scope +" : " return code +"s0." +scope } // core function compile(source) { var inverted = 0, depth = 0 // context stack depth var compiled = new Function("s0", "print", "_isArray", "var out = '" +source .replace(/\\/g, "\\\\") // escape \ .replace(/'/g, "\\'") // escape ' .replace(/\{\{([\^#/!&]?)([^{\n]+?)\}\}/g, function (a, flag, scope) { // block switch (flag) { case '^': // if not inverted++ return "'; var value = " +get(scope, depth) + " ; if (!value || (_isArray(value) && value.length === 0)) { out += '" case '#': // if/each/TODO lambdas/TODO helper return "'; var value = " +get(scope, depth++) + " ; var list = value ? _isArray(value) ? value : [value] : []" + " ; for (var i=0, len=list.length; i<len; i++) {" + " ; var s" +depth +" = list[i]; out += '" case '/': // close inverted > 0 ? inverted-- : depth-- return "'} out += '" case '!': // comments return "" //case '>': // TODO partials case '&': // print noescape return "' +print(" +get(scope, depth) +", false) +'" default : // print escape return "' +print(" +get(scope, depth) +", true) +'" } }) .replace(/\n/g, "\\n") // escape cr +"'; return out") var template = function (data) { return compiled(data, print, _isArray) } return template.render = template // render api } compile.compile = compile // compile api // exports if (typeof module !== 'undefined' && module.exports) { module.exports = compile // CommonJS } else if (typeof define === 'function' && define.amd) { define('lt', compile) // AMD } else { root['lt'] = compile // <script> } })(this) </script>

Test runner

Warning! For accurate results, please disable Firebug before running the tests. (Why?)

Java applet disabled.

Testing in CCBot 2.0.0 / Other 0.0.0
Test Ops/sec
Mustache.js
for (var i = 0; i < tests.length; i++) {
  compiled["Mustache.js"][i](tests[i].variables);
}
pending…
Handlebars.js
for (var i = 0; i < tests.length; i++) {
  compiled["Handlebars.js"][i](tests[i].variables);
}
pending…
Hogan.js
for (var i = 0; i < tests.length; i++) {
  compiled["Hogan.js"][i].render(tests[i].variables);
}
pending…
templayed.js
for (var i = 0; i < tests.length; i++) {
  compiled["templayed.js"][i](tests[i].variables);
}
pending…
LT
for (var i = 0; i < tests.length; i++) {
  compiled["lt"][i](tests[i].variables);
}
pending…

Compare results of other browsers

Revisions

You can edit these tests or add even more tests to this page by appending /edit to the URL.

1 Comment