RND - fast and simple JS template system

As web applications get more dynamic and complex, it's crucial to know what's the fastest way to render content. I have done some research and I am gladly sharing it with you. The things I cover:
  • HTML render benchmarking: test of basic DOM, AJS DOM, innerHTML and RND template.
  • RND template: Python inspired template system that's super simple and super fast.
  • Real world usage: How I gained 80x performance, while getting much cleaner code!

HTML render benchmarking

There are different ways of adding/manipulating HTML, those that I am going to test are following:

  • Basic DOM: Using document.createElement(...).
  • AJS DOM: From AmiJS. Example: AJS.DIV(...).
  • innerHTML: elm.innerHTML = ...
  • RND template: Template system that we will cover later on.

I have benchmarked in following browsers:

  • IE 6 (Windows)
  • FireFox 1.5.0.6 (Mac OS X)
  • Safari 2.0.4
  • Opera 9.0.0 (Mac OS X)

Basically we insert following HTML around 10000 times using the different methods:

<span class="test">
  babble, babble, bitch, bitch
  <div><img src="hej.png" /></div>
</span>

The results

A table over my benchmarks:
Rendering benchmarks

As you can see, using DOM is slow. While using innerHTML is the fastest method.

All around, it's around 3 times faster using innerHTML than DOM. Remember, this is for very basic HTML.

IE was tested on a 3GHz machine with 1 GB ram. The others were tested on a Powermac 2x2.0 GHz G5 with 2 GB ram.

Why not to use innerHTML

Using innerHTML approach can be dirty, mostly because HTML code is spread around. That's when I got the idea to a simple JS template engine. I named it RND and by the benchmarks you can see that it's almost as fast as using innerHTML!

RND JS template

Let me start off by showing the implementation, because it's simple and beautiful:

function RND(tmpl, ns) {
  var fn = function(w, g) {
    g = g.split("|");
    var cnt = ns[g[0]];
    for(var i=1; i < g.length; i++)
      cnt = eval(g[i])(cnt);
    return cnt || w;
  };
  return tmpl.replace(/%(([A-Za-z0-9_|.]))/g, fn);
}

An example of usage

This JS code:

var tmpl = '<a href="%(link)">%(value|parseInt)</a>';
var name_space = {'link': 'http://amix.dk', 'value': 5.5};
alert( RND(tmpl, name_space) );

Will alert:

<a href="http://amix.dk">5</a>

The syntax

In a string you use %(NAME) to denote the placement of placeholders. In the example you can see that placeholders are %(link) and %(value|parseInt). Notice the use of filter function parseInt, i.e. you can pass any functions as filters and you can combine filters.

To fill your template you call RND(tmpl_string, name_sapce), where

  • tmpl_string: A string that contains placeholders.
  • name_space: A JSON object that contains the values of placeholders.

The reasons why RND is good:

  • Good performance
  • HTML representation can be separated from the actual data
  • Templates can be reused

A more complex example

In the benchmarks I used following template:


tmpl = 'Time run: %(time_run)';
tmpl += 'Number of iterations: %(iterations)';
tmpl += 'Basic DOM: %(t_b_dom|getAvg|parseInt) ms';
tmpl += 'AJS DOM: %(t_ajs_dom|getAvg|parseInt) ms';
tmpl += 'innerHTML: %(t_inner|getAvg|parseInt) ms';
tmpl += 'RND template: %(t_rnd|getAvg|parseInt) ms';

Real world usage of RND

Skeletonz has a very nifty plugin syntax builder. The HTML is rendered from JSON and the HTML building was done using AmiJS DOM.

Before I changed the slowest method to use RND I took some benchmarks.

To render plugin syntax HTML using AmiJS DOM:

  • around 950 ms!

To render plugin syntax using RND:

  • around 12 ms!

That's around 80x improvement ~ the code is also much cleaner since HTML representation and actual code are separated.

I am suprised by this result, but happy that I found a solution :)

The tests for Skeletonz were done only in Firefox, using the same benchmarking suite as you'll find in the zip below.

Download

Update [13. Aug 2006]

Code · Code improvement · JavaScript 11. Aug 2006
51 comments so far

My friend Ahmed has submitted this blog post to Digg.
If you like it, you can digg it.

Opera 9 on win2k, doing ten tests and averaging the results.

DOM: 469
AJS: 889
innerHTML: 86
RND: 470

Never once did the RND template beat the basic dom, but It was very close, within 10ms.

I just did a test on Opera 9 for Win XP:
Time run: 10
Number of iterations: 10000
Basic DOM: 2930 ms
AJS DOM: 4337 ms
innerHTML: 1058 ms
RND template: 2561 ms

RND isn't as fast, but it's faster than DOM. I can also assure you that the performance decreases exponentially the more complex the DOM structure gets.

QuirksMode did also some testing a while back that is interesting:

Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6

Time run: 10
Number of iterations: 1000
Basic DOM: 1819 ms
AJS DOM: 2454 ms
innerHTML: 1427 ms
RND template: 1800 ms

XP1800@1917MHz 1gig ram

Time run: 10
Number of iterations: 1000
Basic DOM: 1336 ms
AJS DOM: 1555 ms
innerHTML: 749 ms
RND template: 1023 ms

Perhaps I've missed something, but to me it looks like this test isn't completely accurate...

Your testBasicDOM() function uses innerHTML and the AJS.RCN function, neither of which are "basic DOM" for the purposes of your test. Instead, you should use div.createTextNode in place of the innerHTML and not call an external function for replacing child nodes. Also, the RCN function contains a while loop, which is notorious for being slow in IE. This not only skews your results in RND's favour but it also makes your basic DOM test invalid as a benchmark and as a research result.

The comparison between RND and innerHTML is likewise unfair; if RND has to use innerHTML to insert its data then it cannot be compared directly to a plain innerHTML function. In your test they are ultimately doing the same thing - outputting a string into the DOM via innerHTML - but RND has the added process of a string method (split), a for loop and a regexp replace. However, unlike your simple DOM test, this skews the results against your RND function - RND will never be faster than innerHTML as it is ultimately just innerHTML + a little object and string parser.

Still, this is a great little function. It's clean, straightforward, the template syntax is designer friendly, it accepts JSON data and it obviously isn't a significant performance drain.

Aww Matt you beat me to it. I'm actually rewriting it to use pure dom in the dom example. Is it still worth it?

:P

Oh well, it's fun. I'll continue along my path. :)

First I should note that I do not dislike RND. I think the implementation is short, to the point, and makes imaginative use of JSON.

I only hold issue with one small portion of the benchmark, and I was bored. :)

I made the following assumption when modifying the basic DOM test.

I assumed that the testing loop was was supposed to overwrite any and all (read: pretend I don't know how many elements are in there) internal HTML elements from the experimental element before inserting the new stuff on each loop.

For test-fixed.html:
To be fair I implemented this in a loop, much like AJS's function would have. This approach avoids a bunch of processing as part of AJS.RCN function call which might skew the results, and has the same effect. Please please please correct me if I'm wrong!

For test-unfairdom.html:
Just for fun, I decided to create an unfair example (or what I considered unfair) and created a version which removes the two child nodes of the test element if there are any nodes. Slightly less processing but not much of a difference. This unfair example was created as a response to Matt's note that a while loop is slow in IE. I didn't know this! So I created one without a while loop.

For both:
Since we're using the DOM (as Matt noted above) I modified the inner HTML assignment to use the document.createTextNode call.

Other changes:
Anonymous functions were moved into their own functions to help with comparing the resulting text values.


I refuse to report my own benchmark results as my computer isn't ideal for doing so.

I encourage you to test it yourself using the file below.

The file below includes the version of the test.html file I compared against.

However, I am interested in what anybody else gets with my version of the benchmark.

You can find the file at the following address:
http://rapidshare.de/files/29085224/html_rnd_benchmark-updated.zip.html

Please critique me if you have issues with my modifications, I like to learn from my mistakes!

I almost forgot, I also increased the number of times the test is run from 10 to 20, you know, just for the heck of it. :)

Go for it, it's always worth seeing the pure numbers.

I'd also like to see a more direct comparison with plain innerHTML. At the moment, the RND test creates two templates[1], creates an object and then sends them to the RND function. The innerHTML example simply assigns cnt.innerHTML a simple string. A more accurate test would be to create the templates and data object outside the test functions and then have both RND and innerHTML tests take the data object and create their respective innerHTML strings from it.

This would also be closer to a real-world test, where either RND or innerHTML would be used to output some data received in JSON format. There's no point in using a dynamic template if you're only outputting static data.

[1] this second template even calls the RND function prior to assignment. The ns object then refers back to the second template and this is parsed and inserted into the first template when it gets parsed itself. This is a good example of nesting and reusing templates, which is a pretty cool feature, but again it's slowing down RND in the direct comparison to innerHTML.

"IE was tested on a 3GHz machine with 1 GB ram. The others were tested on a Powermac 2x2.0 GHz G5 with 2 GB ram."

You didn't even use the same platform to test between browsers. Not exactly the sign of someone who cares about accurate results.

Also, a much larger variety of computer/browser configurations should be tested.

Nice one Joe, that's much closer to an accurate test.

Like I mentioned in my last comment, I still think the test is a bit skewed. The basic DOM, AJS and innerHTML tests are all being measured while writing out static content, but RND is dynamically assembling its templates from the "ns" object. All four tests should pull their content from the one object. Obviously, simply swapping strings for object.references won't add much overhead, but that's when you can start comparing features; ie, functions would have be written into the other three examples to replace RND's filters.

Itwally: IE has to be tested on a PC, just as Safari has to be tested on a Mac. Firefox would most likely perform better on PC (the Mac version is a little flakey) but Opera is pretty standard across both. Also, there's no real need to test too many platforms; it has long been established that innerHTML is the faster method, the question is just how much does RND slow it down.

Excellent point!

I may just make those changes in a short while just out of curiosity.

Another point to make about innerHTML is that while extremely fast, is not anywhere near safe when you're inserting or overwriting elements containing javascript functionality.

I believe that if you overwrite the innerHTML of an element, who's innerHTML at one point contained DOM nodes which used event handlers, that you lose the reference to those event handlers and thus create a memory leak in some JavaScript implementations.

Brilliant job! Simply brilliant!

@ltwally

He wasn't testing which browser was faster. He was testing which method was faster in a given browser. So he could have tested each browser on different machines, and still gotten meaningful results, as long as he tested each method on the same browser/computer pair.

It's totally fun comparing apples to oranges. So much so that I decided to try and make the non-templated examples mimic the templated examples.

This is my final update.

This is a little bit closer to what I believe Matt was talking about above. It includes the following differences from my original.

1) All uses of the div with the image in it utilize a pre-created object just like how the original RND test created an initial template first.

2) The pre-created object is duplicated before use in the test functions (either by copying the text or by using element.cloneNode(true) ) to try and simulate the action of re-using a template in a non-templated system.

3) Some variables were moved around

4) I'm getting tired, so there may be other differences. Feel free to rip it apart, but I won't update it anymore.

Download here

Not bad but wouldn't using direct html be faster than anything this could do because it still takes time to render.

one quick point:

can you not replace

cnt = eval(g[i])(cnt);

with

cnt = window[g[i]](cnt)

??? (sorry, I'm adverse to eval)

Matt:
Excellent points. Thanks a lot for the feedback.

Joe:
Great work. I couldn't download the zipz (I also tried from your blog post ).

David Kemp:
I will update my code, thanks! This is a great security update of the code - it's probably also faster.

Ivan Minic:
Hvala puno :)

Anyway, I will redo my benchmark suite. the new test will feature:

  • A lot bigger HTML to build. It will be a 50x50 table that will be filled with data. Events will also be added to the rows.
  • Basic DOM will be rewitten to not use RCN.
  • Every method will recive data from a JSON object to make it more fair.

    I think this will show the true colors of every rendering option. And it will also reasebmles real-world usage.

escape typo in src on webpage:
// return tmpl.replace(/%(([A-Za-z0-9_|.]

  • ))/g, fn);
    return tmpl.replace(/%\(([A-Za-z0-9_|.]
  • )\)/g, fn)

Brendan:
Fixed, thanks!

The benchmark that matters (really):

How fast do search engines find your JSON content?

From here to eternity .... unfortunately.

Could you explain some of the finer details of your cool code please? I've done some research, but haven't worked it all out yet:


Where do the arguments come from when "fn" is invoked?
And what does it mean, to replace a string with a function, anyway?
(replacing a string with the results of invoking a function is OK - that I can understand).

And what does "return cnt || w" mean?
Isn't "||" just OR in javascript?
Or is it more like the || of Perl, meaning it evaluates to cnt if it has a value; else, it evaluates to w?

Time run: 10
Number of iterations: 1000
Basic DOM: 2314 ms
AJS DOM: 3036 ms
innerHTML: 1772 ms
RND template: 2226 ms

(2.4 GHz, 512MB)

PS: on Firefox/1.5.0.6

This line
cnt = eval(g[i])(cnt);
doesn't seem to provide maximum performance, since it uses the eval() function.
Would this be better?
cnt = window[g[i]](cnt);
This way it would directly call the function, instead of evaluating it?

@Amix

If you're at all interested I've updated the links in my blog post not to use rapidshare. If you're curious please feel free to get my two updates there. However, as you are now re-writing it yourself, I would suggest starting fresh and making it clean. :)

Again, well done!

Just out of curiosity, when testing in firefox, does anybody know how to keep it from giving those "script is taking a long time" errors? Is there an option somewhere I can set to keep my results from getting skewed by those dialog boxes?

Joe:
Type "about:config" in the URL bar, find "dom.max_script_run_time" and set it to 0.

@amix

Thanks a lot!

I have updated the benchmarks on my blog to reflect all of my runs, including the run before I knew about dom.max_script_run_time and just clicked 'Continue' as fast as I could like an idiot ;)

An interesting side note for your development efforts. My examples do not seem to produce equivalent HTML in Internet Explorer.

For some reason in IE it's expanding the image path to be an absolute path instead of a relative one for the innerHTML and the RND examples.

This means those two examples have to do a bit more work in IE.

Unfortunately I don't know how to fix this at all. Good luck!

ANS: replace() - Specifying a function as a parameter:
- 1st arg (w above) is the complete matched substring (eg: "%(link)");
- 2nd arg (g above) is from within the parenthesis(eg "link");
NB: there's two parenthesis pair in the regex, one pair is escaped to match those in "%(link)"; the other is nested within it.

var fn = function(w, g) { ... }
return tmpl.replace(/%\(([A-Za-z0-9_|.]

ANS: "||" is Perl-style.

- ... the && and || operators actually return the value of one of the specified operands, so if these operators are used with non-Boolean values, they may return a non-Boolean value.
- Returns expr1 if it can be converted to true; otherwise, returns expr2. Read more...

return cnt || w;



So, if cnt evaluated to an empty string or null in the end (which counts as false), then w is returned instead. I think this is a debugging feature? So we can see what went wrong... if that's its sole purpose, I guess you could flag it too, eg:

return cnt || "___"+w+"___";



I really like your code.

Brendan:
Great writeup. I didn't have time to write one myself, but you pretty much nailed it :)

I also altered your URLs, hope that is ok.

There's only 2 ways to insert HTML elements, as text or objects. I don't see how you can invent another method when the browser simply doesn't expose it. RND has to either end up using innerHTML or appendChild(or it's siblings). That being said, how could it possible be faster if you are doing the same operation but with a regexp match and an extra loop (with an eval also!!!). I think this little function is pretty cool, but there's just no way it's faster than either innerHTML or appendChild alone. Also, for a fair comparison you really need to test some more complicated HTML, those test results look like the layout engine is cheating on Opera and Safari (by that I mean the JS engine and layout engine are smart enough to not keep inserting the same content, who knows what kind of intelligent caching the engine might be doing here. Maybe a test where each iteration of the loop, a slightly unique version of the HTML is inserted, it would be interesting to see if it affected the results of the browsers under 1000ms.


Here is my complaint about innerHTML:

var x = document.createElement('DIV');
x.id = 'wootwoot';
document.body.appendChild(x);


... sometime later
var theDiv = document.getElementById('theDiv');
document.body.innerHTML += 'yoyoyoy';

/*
theDiv is no longer valid, innerHTML += is evil!!!!!!!!! it's not apparent at first, but we are resetting body.innerHTML completely, so all references are lost. innerHTML is fine but be very careful because the layout engine reparses the property if you use +=, which might not be obvious.

  • /

The results:
Time run: 10
Number of iterations: 1000
Basic DOM: 758 ms
AJS DOM: 1069 ms
innerHTML: 495 ms
RND template: 1122 ms

The system:
Machine Name: MacBook Pro 17"
CPU Type: Intel Core Duo
Number Of Cores: 2
CPU Speed: 2.16 GHz
L2 Cache (shared): 2 MB
Memory: 2 GB
Bus Speed: 667 MHz

The application:
Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.0.6)
Gecko/20060728
Firefox/1.5.0.6
(about 15 extensions installed)

So then, why so slow??!?!

Thw AmiFormat didn't seem to have a way to do hyperlinks (according to the reference link below, AFAICS anyway) - is there a way for commentors to add them?
PS: Love the code format (bordered colour).

Bug: if a value of "0" is given, it is registered as a fail, because of

return cnt || w;

Brendan:
I fixed this in the newest version found here.

I will update the documentation of AmiFormat. You use links like this: "Link text":(http://).

How about a DOM template system? Create a template branch once, clone it whenever a new one is needed, adjust values in the clones, and graft.

Excellent use of templates, lambdas and regexp
Very good.
But...
It is asking for XSS (Cross-site-scripting) with the eval of an arbitrary string. You could avoid that having with your RND function a predefined set of transformations valid to the value (parseInt,parseFloat,parseDate,urlEnc,urlDec, etc.)instead of the "eval".

And if you think XSS is not an issue, look at this year Black Hat, where they showed how is possible to control a wireless router with XSS.

Cheers
Skal

Seb:
I also thought about this, but I think traversing the DOM tree to fill it up can be very costly. But it should definitely be tried!

Skal:
I updated the code to not use eval, check it out here.

Doesn't innerHTML get less performant as more elements are on the screen? So if I've got maybe 2000 elements on the screen, innerHTML will cause the page to be re-parsed.

Will this be just as performant if I'm building the entire world of bubble bobble in DHTML?

Kris:
innerHTML performs better, even with a lot of elements.
In my new benchmarking I render a table with 2500 cells. Read more.

comment

I can't seem to get this work. the replacing of variables doesnt work.

output : "%(value|parseInt)"

Code :

<script>
function RND(tmpl, ns) {
var fn = function(w, g) {
g = g.split("|");
var cnt = ns[g[0]];
for(var i=1; i < g.length; i++)
cnt = eval(g[i])(cnt);
return cnt || w;
};
return tmpl.replace(/%(([A-Za-z0-9_|.]))/g, fn);
}

var tmpl = '%(value|parseInt)';
var name_space = {'link': 'http://amix.dk', 'value': 5.5};
alert( RND(tmpl, name_space) );

</script>

I really dig your templating dingus. I didn't need the optional formatting arguments (like parseInt) and I prefer ruby-style variable substitution #{like_this} so I made a preposterously small templating function:

var Templort = function(template, keys) {
return template.replace(/\#\{(.*?)\}/g, function(x,y) { return keys[y]});
};

Great work. Thanx!

HTML render benchmarking: test of basic DOM, AJS DOM, innerHTML and RND template.

Money should not be your drive, but there is a lot of money out there for CS people. Unemployment for CS graduates is around "0%" in Denmark and it's a very well payed job.

عقارات السعودية - عقارات الرياض - عقارات الخرج - عقارات مكة المكرمة - عقارات جدة - عقارات الطائف - عقارات المدينة المنورة - عقارات ينبع - عقارات الدمام - عقارات الخبر - عقارات الأحساء - عقارات القصيم - عقارات عسير - عقارات حائل - عقارات تبوك - عقارات الباحة - عقارات الحدود الشمالية - عقارات الجوف - عقارات جازان - عقارات نجران - عقارات مصر - عقارات القاهرة - عقارات الجيزة - عقارات حلوان - عقارات 6 اكتوبر - عقارات الاسكندرية - عقارات الساحل الشمالي - عقارات البحيرة - عقارات السويس - عقارات الاسماعلية - عقارات بورسعيد - عقارات البحر الاحمر - عقارات مطروح - عقارات جنوب سيناء - عقارات شمال سيناء - عقارات دمياط - عقارات الدقهلية - عقارات كفر الشيخ - عقارات الشرقية - عقارات الغربية - عقارات القليوبية - عقارات المنوفبة - عقارات الفيوم - عقارات قنا - عقارات المنيا - عقارات اسيوط - عقارات بني سويف - عقارات سوهاج - عقارات الوادي الجديد - عقارات الأقصر - عقارات اسوان - عقارات الامارات - عقارات ابوظبي - عقارات العين - عقارات دبي - عقارات جبل علي - عقارات الشارقة - عقارات رأس الخيمة - عقارات عجمان - عقارات أم القيوين - عقارات الفجيرة - الوطن العربي - عقارات الكويت - عقارات عمان - عقارات قطر - عقارات البحرين - عقارات الاردن - عقارات لبنان - عقارات المغرب - عقارات السودان - عقارات سوريا - وظائف - وظائف في السعودية - وظائف في الامارات - وظائف في مصر - وظائف في الكويت - وظائف في عمان - وظائف في قطر - وظائف في البحرين - وظائف في الاردن - وظائف في لبنان - وظائف في المغرب - وظائف في السودان - وظائف في سوريا - خدمات - العروض الجديدة - الرعاية و الإعلان - الدعم الفني - العقارات العام - شراء شقق - شراء شقة - شقق بالتقسيط - شقة بالتقسيط - شقق تمليك - شقة تمليك - شقق سكنية - شقة سكنية - شقق فندقية - شقة فندقية - شقق للايجار - شقة للايجار - شقق للبيع - شقة للبيع - شقق مفروشة - شقة مفروشة - غير مصنف - مطلوب شقق - مطلوب شقة
اهداف الدوري الأنجليزي
اهداف الدوري الأسباني
اهداف دوري ابطال اوروبا
مهارات ولقطات منوعة
اهداف الدوري السعودي
اهداف تصفيات كأس العالم
اهداف الدوري المصري
اهداف الدوري الالماني
أهداف كأس الخليج
هدف تيوب

Post a comment
Commenting on this post has expired.
© 2000-2009 amix. Powered by Skeletonz.