If you've ever tried to use Django-style templates in the JavaScript ecosystem, you know the pain. Nunjucks is the go-to option, but it's slow and missing features. I needed something better for a Bun project, so I built binja.
The Problem
I was migrating a Django application to Bun/Elysia and hit a wall: my templates relied heavily on Django Template Language features that Nunjucks either didn't support or handled poorly. Template inheritance worked, but many filters were missing, and performance wasn't great.
What binja Does Differently
Runtime rendering is 2-4x faster than Nunjucks across all benchmarks:
| Benchmark | binja | Nunjucks |
|---|---|---|
| Simple Template | 371K ops/s | 96K ops/s |
| HTML Escaping | 985K ops/s | 242K ops/s |
| Nested Loops | 76K ops/s | 26K ops/s |
But the real win is AOT compilation. Pre-compile your templates at server startup and rendering becomes synchronous and incredibly fast:
import { compile } from 'binja'
// Compile once at startup
const templates = {
home: compile(await Bun.file('./views/home.html').text()),
user: compile(await Bun.file('./views/user.html').text()),
}
// Sync rendering - no await needed
app.get('/', () => templates.home({ title: 'Welcome' }))
AOT-compiled templates run at 14+ million ops/sec for simple templates. That's ~160x faster than Nunjucks.
Full Django/Jinja2 Compatibility
This was my main goal. binja includes:
- 84 built-in filters covering both Jinja2 and Django
-
28 built-in tests (
is even,is defined, etc.) - All Django loop variables (
forloop.counter,forloop.first,forloop.parentloop) - Django-specific tags:
{% csrf_token %},{% cycle %},{% regroup %},{% lorem %} - Template inheritance with
{% extends %}and{% block %} - Autoescape enabled by default
If you're porting Django templates, most should work without changes.
Multi-Engine Support
A feature I didn't expect to need but ended up loving: binja can parse Handlebars, Liquid, and Twig syntax too.
import * as handlebars from 'binja/engines/handlebars'
import * as liquid from 'binja/engines/liquid'
await handlebars.render('{{#each items}}{{this}}{{/each}}', { items: ['a', 'b'] })
await liquid.render('{% for item in items %}{{ item }}{% endfor %}', { items: ['a', 'b'] })
All engines share the same optimized runtime, so you get consistent performance regardless of syntax.
Framework Integration
First-class adapters for Hono and Elysia:
import { Elysia } from 'elysia'
import { binja } from 'binja/elysia'
const app = new Elysia()
.use(binja({ root: './views', cache: true }))
.get('/', ({ render }) => render('index', { title: 'Home' }))
.listen(3000)
Getting Started
bun add binja
Basic usage:
import { render } from 'binja'
const html = await render('Hello, {{ name|title }}!', { name: 'world' })
// Output: Hello, World!
With an environment for template inheritance:
import { Environment } from 'binja'
const env = new Environment({
templates: './templates',
autoescape: true,
})
const html = await env.render('pages/home.html', { user })
When to Use binja
- Migrating Django/Flask apps to Bun
- Need high-performance server-side rendering
- Want a single template engine that handles multiple syntaxes
- Building with Hono or Elysia and need proper template support
Check it out:
- GitHub: github.com/egeominotti/binja
- npm: npmjs.com/package/binja
PRs welcome.
What template engine are you using with Bun? I'd be curious to hear about your setup.
Top comments (0)