Template engines are a fundamental building block of web development, yet most JavaScript implementations sacrifice performance for compatibility. Today, I'm excited to share binja - a Jinja2/Django template engine built from the ground up for Bun that's 2-4x faster than Nunjucks at runtime, and up to 160x faster with AOT compilation.
This is a project I've been working on alongside flashQ, and it's now ready for production use.
Why Another Template Engine?
When building server-rendered applications with Bun, I found existing options lacking:
- Nunjucks: Great compatibility, but slow and designed for Node.js
- EJS: Fast but limited features, no template inheritance
- Handlebars: Logic-less philosophy doesn't fit all use cases
I wanted the power of Jinja2/Django templates with the speed that Bun deserves. So I built binja.
Benchmark Results
Tested on Mac Studio M1 Max with Bun 1.3.5:
| Scenario | Nunjucks | Binja | Speedup |
|---|---|---|---|
| Simple template | 94K ops/s | 366K ops/s | 3.9x |
| Complex template | 22K ops/s | 45K ops/s | 2.0x |
| Multiple filters | 39K ops/s | 153K ops/s | 3.9x |
| HTML escaping | 71K ops/s | 294K ops/s | 4.1x |
But the real magic is AOT compilation:
| Scenario | Binja Runtime | Binja AOT | Speedup |
|---|---|---|---|
| Simple template | 366K ops/s | 14.3M ops/s | 39x |
| Complex template | 45K ops/s | 1.07M ops/s | 24x |
| Nested loops | 76K ops/s | 1.75M ops/s | 23x |
AOT compilation pre-compiles templates to pure JavaScript functions at build time. No parsing, no AST walking at runtime - just raw string concatenation.
Quick Start
bun add binja
Simple Rendering
import { render } from 'binja'
const html = await render('Hello, {{ name }}!', { name: 'World' })
// "Hello, World!"
Environment Setup (Recommended)
import { Environment } from 'binja'
const env = new Environment({
templates: './templates',
autoescape: true, // XSS protection by default
})
// Render a template file
const html = await env.render('pages/home.html', {
user: { name: 'John', email: 'john@example.com' },
items: ['Apple', 'Banana', 'Cherry'],
})
AOT Compilation (Production)
import { compile } from 'binja'
// Compile once at startup
const renderUser = compile('<h1>{{ name|upper }}</h1>')
// Execute millions of times - synchronous, no async overhead
const html = renderUser({ name: 'john' })
// "<h1>JOHN</h1>"
Use AOT compilation for templates that are rendered frequently with different data (email templates, notification cards, list items). The compilation cost is paid once, then rendering is nearly free.
Feature Highlights
84 Built-in Filters
Binja includes 84 filters out of the box, covering everything from string manipulation to list operations:
{# String filters #}
{{ name|upper }} {# "JOHN" #}
{{ text|truncatewords:10 }} {# First 10 words... #}
{{ title|slugify }} {# "my-blog-post" #}
{# Number filters #}
{{ price|floatformat:2 }} {# "19.99" #}
{{ bytes|filesizeformat }} {# "1.5 MB" #}
{# List filters #}
{{ items|join:", " }} {# "a, b, c" #}
{{ users|sort:"name" }} {# Sorted by name #}
{{ items|batch:3 }} {# [[1,2,3], [4,5,6]] #}
Template Inheritance
{# templates/base.html #}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Default{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
{# templates/page.html #}
{% extends "base.html" %}
{% block title %}My Page{% endblock %}
{% block content %}
<h1>Welcome, {{ user.name }}!</h1>
{% endblock %}
Control Flow
{% if user.is_admin %}
<a href="/admin">Admin Panel</a>
{% elif user.is_moderator %}
<a href="/mod">Mod Tools</a>
{% else %}
<a href="/profile">Profile</a>
{% endif %}
{% for item in items %}
<div class="{% if loop.first %}first{% endif %}">
{{ loop.index }}. {{ item.name }}
</div>
{% empty %}
<p>No items found.</p>
{% endfor %}
Multi-Engine Support
Binja can also parse and render templates written in other syntaxes:
import { Environment } from 'binja'
// Handlebars syntax
const hbs = new Environment({ syntax: 'handlebars' })
await hbs.render('{{#each items}}{{this}}{{/each}}', { items: [1, 2, 3] })
// Liquid syntax
const liquid = new Environment({ syntax: 'liquid' })
await liquid.render('{% for item in items %}{{ item }}{% endfor %}', { items: [1, 2, 3] })
// Twig syntax
const twig = new Environment({ syntax: 'twig' })
await twig.render('{% for item in items %}{{ item }}{% endfor %}', { items: [1, 2, 3] })
Integration with Hono & Elysia
Binja includes first-class adapters for modern Bun frameworks:
Hono Integration
import { Hono } from 'hono'
import { binja } from 'binja/hono'
const app = new Hono()
// Register binja middleware
app.use('*', binja({
templates: './templates',
autoescape: true,
}))
app.get('/', (c) => {
return c.render('home.html', {
title: 'Welcome',
user: { name: 'John' },
})
})
export default app
Elysia Integration
import { Elysia } from 'elysia'
import { binja } from 'binja/elysia'
const app = new Elysia()
.use(binja({
templates: './templates',
autoescape: true,
}))
.get('/', ({ render }) => {
return render('home.html', {
title: 'Welcome',
user: { name: 'John' },
})
})
.listen(3000)
Using with flashQ
Binja pairs perfectly with flashQ for rendering email templates, notification content, and other background tasks:
import { Worker } from 'flashq'
import { compile } from 'binja'
import { Resend } from 'resend'
const resend = new Resend(process.env.RESEND_API_KEY)
// Pre-compile email templates at startup (AOT)
const templates = {
welcome: compile(await Bun.file('./emails/welcome.html').text()),
invoice: compile(await Bun.file('./emails/invoice.html').text()),
reset: compile(await Bun.file('./emails/password-reset.html').text()),
}
const emailWorker = new Worker('email', async (job) => {
const { to, template, data } = job.data
// Render is synchronous and blazing fast
const html = templates[template](data)
await resend.emails.send({
from: 'noreply@example.com',
to,
subject: data.subject,
html,
})
return { sent: true, to }
}, {
connection: { host: 'localhost', port: 6789 },
concurrency: 20,
})
With AOT-compiled templates, your email worker can render thousands of personalized emails per second with minimal CPU overhead. The rendering step becomes negligible compared to the API call to your email provider.
CLI Tool
Binja includes a CLI for pre-compiling templates and linting:
# Compile all templates to JavaScript
binja compile ./templates --out ./compiled
# Lint templates for errors
binja lint ./templates
# Render a template from CLI
binja render ./template.html --data '{"name": "World"}'
Security
Binja has security built-in:
- Autoescape by default: All output is HTML-escaped unless explicitly marked safe
- No code execution: Templates cannot execute arbitrary JavaScript
- Sandboxed filters: Custom filters run in a restricted context
{# Automatically escaped #}
{{ user_input }} {# <script> becomes <script> #}
{# Explicitly mark as safe (use carefully) #}
{{ trusted_html|safe }}
Get Started
Binja is open source and available on npm:
bun add binja
Give it a try and let me know what you think. Issues and PRs are welcome!
Build Faster with Bun
Pair binja with flashQ for high-performance server-rendered apps with background job processing.
Get Started with flashQ