HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMajor

Templating libraries' intelligibility and their performance (compared to mine)

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
minetemplatingintelligibilitylibrariesperformanceandcomparedtheir

Problem

I've just started writing my own little templating library in JavaScript, because as I went through others, there is always this voice in head, which says: "Oh, this is a lot of code and functionality. Is that really necessary or would it slow down the performance?"

If you are able to follow conventions, you would probably run into less issues.

So I ended up with this little script with the upper quote in my head. The convention, here is that data needs an object and its keys must be the same like in template, mentioned between those delimiter-brackets "{{key}}"

```
function Template(url, data) {

this.url = url;
this.data = data;

this.delimiter = ['{{','}}'];

this.load = function() {

var string = new String();

var http = null;
if ( window.XMLHttpRequest ) {
http = new XMLHttpRequest();
} else {
http = new ActiveXObject("Microsoft.XMLHTTP");
}

http.open('GET', url, false);
http.onreadystatechange = function() {
if ( http.readyState === 4 ) {
string = http.response;
}
};
http.send(null);

this.template = string;
return this;

};

this.exchange = function() {

for ( var key in this.data ) {

var bracketedKey = this.delimiter[0] + key + this.delimiter[1];
var indexOfKey = this.template.indexOf(bracketedKey);
var lengthOfKey = bracketedKey.length;

var exchange = this.template.substring(indexOfKey, indexOfKey+lengthOfKey);

this.template = this.template.replace(exchange, this.data[key]);

}

return this;

};

this.build = function(selector) {

this.load();
this.exchange();

var div = document.createElement('div');
div.innerHTML = this.template;
div = div.firstChild;

if ( typeof selector === 'string' ) {
document.querySelector(selector).ap

Solution

... as I went through others, there is always this voice in head, which says: "Oh, this is a lot of code and functionality. Is that really necessary or would it slow down the performance?"

This is a reasonable consideration, but premature optimisation is evil for a reason. It might be a good idea to try to roll out your own solution, but it would be beneficial to learn more about the problem and other existing solutions before implementing it yourself.

As for minimalistic templating library, take a look at Underscore _.template.

It's literally 45 lines (add one or two helper functions) and does its job well.

As for your solution, I have a few points:
Don't use AJAX to load templates

If you're worried about performance, loading templates via AJAX is a bad idea. This will be a terrible bottleneck, not some hundred more lines of code in a different templating library.

Moreover, your code assumes you need to make an HTTP request each time you render something. If you have a list view with 20 items, that would be 20 requests.

This is absolutely unsuitable for production code.
Don't use synchronous AJAX calls, like, ever

The very point of AJAX is processing things asynchronously, without blocking other scripts from executing. There is rarely a reason to do things synchronously.

Again, please don't do this in production code.
Instead, put templates in HTML or (better) compile them to JS

Most, if not all, templating libraries assume the template is already available in the client code, and this is the correct way to go. Now, there are two ways how you can accomplish this:

  1. You can embed templates in DOM with tricks like `



The upside is that it is simple (no additional build steps), but the downside is that all your templates will have to be repeatedly passed in HTML with every page.

  1. But really, you should precompile templates to a JS file



In this case, your build workflow will include an additional step when a certain command-line script goes over your
templates/*.html files and compiles each HTML template into a JavaScript function that “takes” data arguments and returns the “rendered” HTML string.

For example, a template like


  

    

    

    {{ caption }}
  


would be compiled in a function like

this["st"]["Template"]["templates/editor/items/gallery_media.html"] = function(obj) {
obj || (obj = {});
var __t, __p = '', __e = _.escape;
with (obj) {
__p += '\n  \n\n    \n\n    \n\n    ' +
((__t = ( caption )) == null ? '' : __t) +
'\n  \n';

}
return __p
};


which would be placed in a generated
templates.js file. This would be blazing fast.

Note that there is no overhead of parsing in this case, because parsing happens on your machine, during the build. The client uses pregenerated functions.

Such functions can be generated by most templating engines.
Don't parse the same template twice

Even if we bundle templates with the HTML (
script type="text/template" approach), your code still suffers from the fact it parses the same template every time it is being rendered. That means, for 20 identical items, the same template is parsed 20 times. Of course, parsing is nothing compared to fetching HTML, but it's something that is relatively straightforward to optimize away.

Instead, you should parse the template once, somehow cache the “parsed” version and figure out how to “apply” it to different models. As I advocated before, the natural way to it is to make
template build a function that corresponds to your template.

This is exactly what Underscore's
_.template does, so you want to look at its implementation (see also an annotated version):

``
_.template = function(text, data, settings) {
var render;
settings = _.defaults({}, settings, _.templateSettings);

var matcher = new RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');

var index = 0;
var source = "__p+='";
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset)
.replace(escaper, function(match) { return '\\' + escapes[match]; });

if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
}
if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
}
if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
index = offset + match.length;
return match;
});
source += "';\n";

if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';

source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + "return __p;\n";

try {
render = new Function(settings.variable || 'obj', '_', source);
} catch (e) {
e.source = source;
throw e;
}

if (data) return render(data, _);
var template = function(data) {
return render.call

Code Snippets

<div class="editor-Gallery-media">
  <div class="editor-Gallery-mediaItem"
    style="background-image: url({{ thumbnail_url }});"
    data-orientation="{{ orientation }}">

    <div class="editor-Gallery-deleteMediaItem"><i class="icon-cross"></i></div>

    <div class="editor-GrabboxMedia-orientation"></div>

    <div class="editor-Gallery-mediaItemCaption">{{ caption }}</div>
  </div>
</div>
this["st"]["Template"]["templates/editor/items/gallery_media.html"] = function(obj) {
obj || (obj = {});
var __t, __p = '', __e = _.escape;
with (obj) {
__p += '<div class="editor-Gallery-media">\n  <div class="editor-Gallery-mediaItem"\n    style="background-image: url(' +
((__t = ( thumbnail_url )) == null ? '' : __t) +
');"\n    data-orientation="' +
((__t = ( orientation )) == null ? '' : __t) +
'">\n\n    <div class="editor-Gallery-deleteMediaItem"><i class="icon-cross"></i></div>\n\n    <div class="editor-GrabboxMedia-orientation"></div>\n\n    <div class="editor-Gallery-mediaItemCaption">' +
((__t = ( caption )) == null ? '' : __t) +
'</div>\n  </div>\n</div>';

}
return __p
};
_.template = function(text, data, settings) {
  var render;
  settings = _.defaults({}, settings, _.templateSettings);

  var matcher = new RegExp([
    (settings.escape || noMatch).source,
    (settings.interpolate || noMatch).source,
    (settings.evaluate || noMatch).source
  ].join('|') + '|$', 'g');

  var index = 0;
  var source = "__p+='";
  text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
    source += text.slice(index, offset)
      .replace(escaper, function(match) { return '\\' + escapes[match]; });

    if (escape) {
      source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
    }
    if (interpolate) {
      source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
    }
    if (evaluate) {
      source += "';\n" + evaluate + "\n__p+='";
    }
    index = offset + match.length;
    return match;
  });
  source += "';\n";


  if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';

  source = "var __t,__p='',__j=Array.prototype.join," +
    "print=function(){__p+=__j.call(arguments,'');};\n" +
    source + "return __p;\n";

  try {
    render = new Function(settings.variable || 'obj', '_', source);
  } catch (e) {
    e.source = source;
    throw e;
  }

  if (data) return render(data, _);
  var template = function(data) {
    return render.call(this, data, _);
  };

  template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';

  return template;
};
var string = new String();
var template;

Context

StackExchange Code Review Q#38435, answer score: 21

Revisions (0)

No revisions yet.