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

Info

Compared Mustache.js, Handlebars.js, Hogan.js , templayed.js and LT.

Add compile benchmark. Here is another benchmark

Preparation code

<script src="//cdnjs.cloudflare.com/ajax/libs/mustache.js/0.7.2/mustache.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/1.0.0/handlebars.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/hogan.js/2.0.0/hogan.js"></script>
<script src="//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

Test runner

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

Java applet disabled.

Testing in unknown unknown
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. Here’s a list of current revisions for this page:

1 comment

tomByrer commented :

Worth while to note:

  • (LT)(https://github.com/rhyzx/lt/blob/master/lt.js) & it seems templayed do not support partials, though I do not see this feature used in most {{templates}}
  • LT could be sped up IMHO by using if/else with likelyhood reordering.

Add a comment