Deno and Preact are really fast
This post began several years ago, when I realized that the page responses for my Django site were taking much longer than I expected, over a second in many cases. Upon investigation, what I found surprised me: several hundred milliseconds were being spent in template rendering. (More time than either database queries or network latency.)
This is interesting, because the conventional wisdom that I had heard was that response time was normally dominated by database queries and network time. This post covers what I learned after realizing that template rendering performance mattered.
Jinja
Jinja is an Python templating system that can be plugged into Django to replace Django's native template language. Some people reported Jinja was faster than Django, so I rewrote my pages with Jinja, and the rendering time was roughly halved. This was a success, but not the end of the story.
For this post, I ran Jinja against all pages on my site and found that each page takes on average 86ms to render.
86.031 ms ± 32.238 ms (37.307 ms .. 148.425 ms)Note that this number excludes querying the database (although the local SQLite database responds within milliseconds).
While faster than Django's template system, 86ms on every request is, in my opinion, unacceptable. I fixed this latency by adding fragment caching for most of the content (like many sites do). But a lingering question remained: what template language is the fastest?
Benchmarks
So I created a benchmark comparing different templating systems. I wanted to see which one you should choose if you wanted to avoid template performance issues.
This benchmark asks different template languages to render a simple template with the same amount of data I see on my site. Here are the results:
React: 8.511 ms ± 0.468 ms (8.164 ms .. 9.886 ms)
Handlebars: 3.251 ms ± 0.060 ms (3.182 ms .. 3.345 ms)
NanoJSX: 2.718 ms ± 0.603 ms (2.479 ms .. 4.524 ms)
Liquid: 2.197 ms ± 0.047 ms (2.117 ms .. 2.287 ms)
Preact (bun): 1.752 ms ± 0.591 ms (1.490 ms .. 3.520 ms)
Preact (node): 1.159 ms ± 0.016 ms (1.131 ms .. 1.182 ms)
Django: 0.961 ms ± 0.020 ms (0.933 ms .. 0.992 ms)
Preact (deno): 0.722 ms ± 0.012 ms (0.697 ms .. 0.736 ms)
Jinja: 0.513 ms ± 0.010 ms (0.496 ms .. 0.532 ms)
Tera: 0.510 ms ± 0.038 ms (0.480 ms .. 0.588 ms)
Go: 0.426 ms ± 0.025 ms (0.396 ms .. 0.469 ms)
ERB: 0.159 ms ± 0.004 ms (0.150 ms .. 0.165 ms)In my opinion the most interesting result in this table is Deno, but I want to talk about a couple of other ones first.
React / Preact
React is normally thought of as a client side library for doing JavaScript based rendering and interactivity in the user's browser. But React and modern React frameworks support server side rendering ("SSR"). When used this way, it functions identically to a templating language, so I'm going to refer to it as a "templating system." React has a reputation for being slow on clients, but I expected it would perform similarly to a traditional templating system when used for SSR (as in the benchmark). For some reason it does not. This is made more shocking by the fact that Preact (which is a drop-in-replacement), on the same runtime (NodeJS) is 7.7 times faster.
"Switch to Preact" has always been good advise for the sake of your client-side bundle size, but it appears to be good advice for improving your SSR performance as well.
Django / Jinja
Since I don't have my exact production numbers for Django templates anymore, I'm glad my recollection of a 2x speedup from Django Template Language to Jinja is reproduced here (0.961 ms -> 0.513 ms).
Jinja, Go, and Tera are all right around the same time. It doesn't look like I'm going to be able to get better performance for my site than Jinja provides.
ERB
At 0.159ms, this is the fastest template system tested. This is Ruby's embedded template language, and Ruby used to be an infamously slow language. I can only guess why it's so fast. Unlike the other candidates (which are 3rd party libraries), ERB is an official Ruby feature. So perhaps the JIT compiler that ships with the latest versions of Ruby contains special optimizations specially for ERB.
Deno + Preact
The reason I bothered making this post was Deno's precompile transform. This is a really cool optimization that other toolchains don't do. But in order to explain it we have to talk a little bit about how JSX works.
Traditional templating languages work by filling in a template string. They do a simple find-and-replace, and don't care about the structure of the surrounding HTML (indeed, they can generate things that aren't HTML).
<span>Hello, <em>{{name}}<em></span>This gives them an advantage in performance compared to the JSX-based options.
React, NanoJSX, and Preact don't work on text. JSX is syntax sugar for defining a tree of nodes (which happens to have an HTML-like syntax). But these libraries see a tree of JavaScript nodes, and they have to render that tree to an HTML string.
React.createElement("span", null, "Hello, ", React.createElement("em", null, {name}));Many developers, like myself, prefer JSX to string-based templating libraries because of the additional ease of working with this tree of nodes directly.
Deno's precompile transform is based on the insight that most of these nodes don't change. So when we're transforming the JSX to runnable JavaScript, we can serialize a lot of this tree into a "template." The above JS was an example of what a traditional bundler targeting React outputs for some simple JSX. The following is an example of what Deno with precompile enabled will output for the same JSX.
var $$_tpl_1 = ["<span>Hello, <em>", "</em></span>"];
var a3 = a2($$_tpl_1, s2({name}));Using magic that I don't understand, Deno is able to construct something that looks exactly like a text-based template from the JSX information present in the source. This gets us the best of both worlds: the convenience of working with a node-tree, and runtime performance very nearly as good as the most performant text-based template systems.
Deno 1.38 Release Notes - Fast(est) JSX transform
Methodology, Background, and Additional Details
The methodology of my benchmark is far from perfect.
At the beginning of this rabbit hole I had a hypothesis that the large amount of text I was rendering into the template was causing the 100ms render timings I was seeing. The benchmark was designed to test this hypothesis, so it runs with a realistic amount of data (around 100KB), but a trivial template. When run on the same VPS where Jinja takes 86ms with the real template, the benchmark generates an HTML document in just 1ms. So my hypothesis is disproved.
Since the benchmark doesn't accurately simulate real world conditions in terms of performance, and performance is the dimension we're trying to measure, I would advise taking the results with a grain of salt. That being said, I don't see anything indicating that the results wouldn't scale to more realistic workloads.
The templating systems I tested were chosen to give a general overview of different programming language ecosystems. Java is, I think, the most notable omission. There are several from the JavaScript ecosystem because I'm personally familiar with them and with the ecosystem (e.g. NanoJSX is extremely obscure, but I've used it on a previous project).
I've made the benchmark code and data available, so you can reproduce my numbers, compare your favorite templating language, or attempt to adapt the template to more realistic conditions.