tempura: a small template engine with two ways to compile
A light, crispy, and delicious template engine 🍤
At a glance
- What is it?
- Tempura is a template engine for JavaScript whose pitch is size and speed, and the interesting design decision is not the syntax but the compilation step. The same template can be turned into a module for a build tool or into a live function for a server, and the difference between those two outputs is a single function call. The rest of the library is small enough that the entire thing fits in about two kilobytes compressed.
- Who is it for?
- tempura fits a project that renders templates in a build step and wants the output to be a module, or a small server that wants a template compiled once and called many times, and who is willing to accept a syntax that borrows from an existing templating language rather than inventing one. It does not fit somebody who wants a large ecosystem of template helpers.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What it is, and the size claim
Tempura describes itself as a light, crispy and delicious template engine, and the metaphor runs through the whole project: the logo is a shrimp, the licence badge comes from a site that serves licence texts, and the author is the same person who built a well known routing library for the same ecosystem.
The feature list has four items and the first is the one that defines the project. The engine is extremely lightweight: everything is about two kilobytes gzipped, and less again with tree shaking.
That number is worth taking seriously for a moment, because it is a different kind of claim from most lightweight claims. It is not that the library does little; it is that the whole thing, including the compiler, the runtime and the escaping, is measured in kilobytes rather than in tens of kilobytes. And the qualification about tree shaking is the interesting part, because it means the package is written so that a bundler can drop the parts you do not use, which is a property of the module structure rather than of the feature set.
The second claim is performance, described as significantly faster than the big names and also faster than the small ones, with a link to the benchmarks in the repository. The third is familiarity: the templates look right with the syntax highlighting that editors already have for a well known templating language, which is a design constraint that is easy to state and hard to honour, because it means the block names and the delimiters have to match another language's conventions without importing that language's semantics.
The fourth is extensibility: custom blocks can be defined through the API to extend the functionality, and there is a separate page documenting plugins.
The installation is one command, and the runtime requirements are stated with unusual precision. It is an ES module for a recent Node and for another runtime, and it can be loaded with an import, or with the older call form on versions of Node that support it and on the other runtime. The readme puts the install command first and the module requirements immediately after, which is the right order for a library whose whole selling point is that it drops into a build with no ceremony.
Two compilation modes, and why the choice is yours
The usage example loads two functions from the library and the difference between them is the most important thing in the readme.
One of them transforms a template into a string of JavaScript source. The readme shows what that string looks like: an import of the escaping helper from the library, and an exported default function taking two arguments. And it says what that output is for: producing a string is ideal for build tools and bundlers.
The other one compiles a template into a live function. And the readme says what that is for: ideal for runtimes and servers.
That distinction is a real design decision rather than a convenience, and it is the kind of decision a template engine usually makes for you. A library that only produced a function would force a server to build a function from a string at startup, which either means evaluating strings at run time, which some environments forbid, or means shipping a compiler to the server. A library that only produced a string would force a build tool to evaluate the module, which is fine but means the build output is not directly a module either.
Producing both from one template is the useful part, because the two consumers want genuinely different artefacts and neither wants the other's.
import { readFile } from 'fs/promises';
import { transform, compile } from 'tempura';
const template = await readFile('example.hbs', 'utf8');
// Transform the template into a function
// NOTE: Produces a string; ideal for build/bundlers
// ---
let toESM = transform(template);
console.log(typeof toESM); //=> "string"And the two-function form is called with the data and returns the rendered string, which is the whole API a user of a template engine needs.
One more detail in the generated output is worth a reader's attention: the emitted module imports the escaping helper under a generated name. The compiler does not inline the escaping, it references the library's own function, which is why the tree shaking works and why the generated module is small.
The syntax, and the decision to look familiar
The example template is a small to-do list, and it is worth reading in full because it shows the whole language in twenty lines.
There is a declaration of what the template expects to receive, which is the first block in the file. There are inline variables, declared with a name and a value, and the example uses a conditional expression to pick between two words depending on a count, which tells you the expression syntax is a full expression language rather than a restricted one.
Then there is a conditional block, with an else branch and a nested conditional inside it that uses an else-if chain, and the nesting is where a template engine either earns its keep or falls apart, because a nested block inside a branch has to keep its own scope and its own closing tag.
Then there is a loop, which binds an alias for the current item, and the alias is used to reach a property of the item.
And then the output syntax, where the triple braces mean the value is not escaped, and the double braces mean it is. That is the one detail in the whole language that matters most in practice, because it is the difference between a template engine that is safe by default and one that is not.
The comment syntax is a hash inside the braces, which is what the example uses to annotate the template with what it expects to receive.
What is notable is that the whole language is a small, familiar set, and the readme's decision to point at a cheatsheet rather than a reference manual says the language is small enough for a single page. The features list claims the syntax looks right with the highlighting your editor already has for a different templating language, and having read the example you can see that this is a real constraint and not a figure of speech: the block names, the delimiters, the nesting and the alias syntax are all borrowed, so that a file written in this language is readable to somebody who has only ever used the other one.
The trade-off of that decision is that the language inherits the other one's blind spots as well as its strengths, and the answer to that is the extension mechanism, which is where the next section goes.
The benchmarks, and what they actually show
The benchmark section is admirably honest about its own limits, and the disclaimers are the first thing you read. It says the results are running on a particular runtime version, it points at a directory in the repository for the complete and reproducible benchmarks, and it says the numbers shown are a subset presented without context.
That is better than most benchmark sections, which present a table and let the reader assume it is the whole story. Here the reader is told, in the author's own words, that the table is out of context, and then is given the context.
The subset is two benchmarks: one rendering raw values with no escaping, and one rendering escaped values.
Benchmark: Render w/ raw values (no escape)
pug x 34,847 ops/sec ±2.79% (93 runs sampled)
handlebars x 6,700 ops/sec ±1.41% (92 runs sampled)
ejs x 802 ops/sec ±0.54% (94 runs sampled)
dot x 40,704 ops/sec ±3.08% (93 runs sampled)
art-template x 39,839 ops/sec ±0.86% (90 runs sampled)
tempura x 44,656 ops/sec ±0.42% (92 runs sampled)Two things are visible here. The first is that the field splits into two groups, with three engines in the tens of thousands and two engines far below them, and the difference is roughly a factor of fifty between the top and the bottom of the range. The second is that the top three are within about fifteen percent of each other, so the ranking among them is a technicality; what matters is the group.
And then the escaped benchmark is the more interesting of the two, because the ordering changes. The three leaders fall to between about two thousand seven hundred and two thousand nine hundred operations a second, one of the two laggards gains, and the other stays low. So the escaping step, not the templating, is what costs.
That is a genuinely useful thing to know, and it is the kind of insight a benchmark subset can convey when the author is careful. It also puts the library's own numbers in context: it leads in both, but the margin in the escaped case is a few percent over the nearest competitor rather than the large margin the raw numbers suggest.
The caveat in the note at the top is the one that matters most for a reader planning to rely on these numbers, and it is that they were measured on a runtime version the library no longer supports. The library now asks for a recent Node, and the benchmark was run on an older one. The comparison is therefore an indication of the shape of the field rather than a current ranking.
Extending it, and where the project has ended up
The documentation section lists four pages, and the set of them is the library's actual surface. There is a syntax cheatsheet, an API page, a page on custom blocks, and a page on plugins.
The distinction between the last two is worth drawing out, because it is the difference between extending the language and extending the implementation. A custom block is a piece of syntax the author defines and the template can then use, so it changes what a template can say. A plugin is a hook into the engine's own operation, so it changes what the engine does. A library that offers both lets an application say what it needs in the template language and how it should behave underneath, without the library having to guess either.
And the examples directory and the benchmark directory are both in the repository, which is the standard arrangement for a library whose documentation is mostly code. The readme points at both from the usage section, and the benchmark section points at the benchmark directory for the reproducible versions with the explanations, the comparisons and the differences between the engines. That last point is the one I would underline: the directory is not just the raw numbers, it is the explanation of why the numbers differ, which is where the real content of a comparison lives.
The licensing and authorship are simple: a permissive licence, the author's name, and a link to his site. The badges are the standard set, and one of them points at a licence text service, which is a small nicety that means the licence is readable in the package listing rather than only in the repository.
What is more interesting is the chronology. The last release in the history is from 2021, and the last push to the repository is from 2026. So the project is being touched without being released, which usually means one of two things: it is used in production somewhere and maintained quietly, or the author is working on it in a way that has not produced a release yet. Combined with the fact that the runtime requirement has moved forward to a recent Node while the benchmark still names an old one, the picture is of a small library that keeps working and keeps getting touched, with a published version that is a couple of years behind its own documentation.
Which is a perfectly reasonable thing for a library of two kilobytes to be. There is very little to break, and the people using it are a small number of people who know it well.
Editorial conclusion
tempura fits a project that renders templates in a build step and wants the output to be a module, or a small server that wants a template compiled once and called many times, and who is willing to accept a syntax that borrows from an existing templating language rather than inventing one. It does not fit somebody who wants a large ecosystem of template helpers. Verify first which of the two compilation modes your build needs, since the string output and the live function are not interchangeable, that the syntax you need is in the cheatsheet rather than assumed, because the library deliberately offers a small set of blocks, and that the published benchmark numbers are treated as indicative, since they were measured on a runtime version the library no longer supports.
Frequently asked questions
What is the tempura template engine?
It is a small template engine for JavaScript with a syntax borrowed from a well known templating language, so that templates read the way people expect and get the syntax highlighting their editor already has. It supports declaring expected properties, inline variables, conditionals with else-if chains, loops with an alias, and unescaped output through triple braces.
What is the difference between transform and compile in tempura?
One of them turns a template into a string of JavaScript source, which is what a build tool or bundler wants, and the other turns it into a live function, which is what a server or a runtime wants. The same template can be produced either way, and the generated module imports the library's escaping helper rather than inlining it.
How small is tempura?
The readme says everything is about two kilobytes gzipped, and less with tree shaking, which is the whole engine including the compiler, the runtime and the escaping. The published benchmark numbers are presented as a subset without context, with the complete reproducible runs in the repository.
What do the tempura benchmarks show?
Two tables, one rendering raw values with no escaping and one rendering escaped values. The first splits the engines into a leading group of three and a much slower pair, and the second shows that escaping rather than templating is where the cost is: the leaders fall to between about two thousand seven hundred and two thousand nine hundred operations a second. The results were measured on a runtime version the library no longer supports.
How can tempura be extended?
In two ways, with separate documentation pages for each. Custom blocks extend the template language itself, so a template can use new syntax, and plugins hook into the engine's behaviour. The benchmark directory also holds the explanations and comparisons behind the published numbers, not just the raw results.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/lukeed-tempura)Was this useful?