Skip to content

Defining template engine performance

pyykkis edited this page Jul 12, 2012 · 50 revisions

Note: There's ongoing discussion at Reddit on how client-side template engine performance should be measured. This article and the test cases are updated accordingly.

Context is everything

To measure a performance, one should first define the task. After that, the performance over that specific task can be measured. Clearly, any tool used for the task should produce the same end results, only with different performance.

Regarding template engines, there's no general 'performance' to be measured. It's either client-side performance or server-side performance.

At the server-side, the end result is a HTML string with rendered data values, e.g., something that Jade, Handlebars or numerous other string-based template engines provide. After rendering, the HTML string is sent to the browser via HTTP.

At the client-side, HTML string provided by a template engine is just a part of the story. The goal at the client-side is to show the results to the user, i.e., visible DOM elements with the data. For string-based template engines, it means that the result string needs to be set to .innerHTML of the parent element. That's a significant part of the measurement, which is often forgotten.

This article focuses on client-side performance, as that is the primary target for Transparency.

How browsers work from template engine point of view?

For string-based template engines, there's more or less just one relevant interaction with the browser. That is, assigning the HTML snippet to .innerHTML of the parent element. At that point, the browser discards all the existing child nodes, parses the HTML snippet to DOM elements and appends them as child nodes to the parent element. Naturally, if the parent element is visible, the operation also triggers reflow calculations.

With string-based templates, parsing HTML string to DOM objects with .innerHTML takes 80% to 90% of the total rendering time. As template engines can't optimize that, it really doesn't matter much which is faster, Mustache, Handlebars or Hogan, if you use them on the client-side. Even though one might be faster in generating HTML strings, overall they have about the same performance.

With DOM-based template engines, there's many interactions with the DOM API. Identifying the costly ones opens doors for optimizations. At least following actions have significant cost

  • Manipulating visible elements (triggers reflow calculations)
  • Reading and writing .innerHTML, .className or other attributes, even if the elements are detached from DOM
  • Creating and removing DOM elements, even if the elements are detached from DOM
  • Running document.getElementsByTagName and similar queries

Naturally, performance characteristics varies between browser implementations. From the template engine point of view, main browser differences in the test cases can be explained by

  • The speed and optimization strategies of the JIT engine
  • The speed and optimization strategies of the regular expression engine
  • The speed and optimization strategies of the DOM implementation

Optimization techniques employed in Transparency

Terminology

  • Context: The target node for .render call, e.g., $('#template').render(data)
  • Data: The data to be rendered. It might be a single model or list of models.
  • Model: Plain javascript object, i.e., key-value pairs. Keys are used to match values to the template elements.
  • Template: Original child nodes of the context. Never used for actual rendering.
  • Template instance: Cloned from the template and used for rendering a single model. The number of instances matches to the number of models.
  • Instance cache: Instances which are not in use at the moment.
  • Query cache: Instance specific cache for queries like template.querySelectorAll(key).

Techniques

Minimize manipulation of the visible DOM elements. By far the easiest optimization. Transparency implements this by detaching the context node before manipulations and attaching it back to its place after all the manipulations have been done. This way, only two reflow calculations are triggered, even when rendering a list of hundreds of models.

Create new DOM elements only if needed and keep them forever. Let's imagine we've a to-do application with ten items on the list. You complete most of them and add a few more. Under the hood, Transparency detaches the template instances (i.e. list elements) from the DOM as you complete the tasks. However, it keeps the unused instances in the instance cache. When you add new items, Transparency first uses the cached template instances. Only if there's not enough instances in the cache, new ones are created by cloning the template.

Cache query selector results. Executing query selectors like template.querySelectorAll(key) is potentially expensive. So, Transparency saves the results to a query cache. The query itself, as well as the cache, is specific to a given template instance. This enables Transparency to render a list of models without template instances interfering each other. The query cache is initialized when the instance is used for the first time, which means that the existing template instances are faster to use than the new ones. This is an important reason to keep and reuse instances until the end of the world.

Update element attributes only if needed. Finally, getting and setting text, html or any attributes on DOM elements has a non-trivial cost, which is significantly larger than getting or setting a value of a plain javascript object. For that reason, Transparency saves the data values to its own data structures, remotely similar to jQuery.data() implementation. If there's no changes in the data between .render(data) calls, Trasparency doesn't even touch the DOM elements.

Requirements for the test cases

  1. Performance matters only with large amount of data
  2. Template data needs to be random for each iteration
  3. Template engines should produce DOM elements. HTML strings needs to be parsed to DOM elements.
  4. Rendering target should be a detached element in order to avoid variance due to reflow calculations
  5. After benchmark, rendering results should be attached to DOM and shown to the user in order to validate the output easily

As Transparency builds its cache on first rendering, there's a significant performance difference between the first .render call and the subsequent calls. Thus, its first-time rendering performance should be measured separately from the subsequent calls.

Test cases

Transparency vs. Handlebars infinite lists
http://jsperf.com/transparency-vs-handlebars-infinite-list/9

Transparency vs. Handlebars finite lists
http://jsperf.com/transparency-vs-handlebars-finite-list/11