Features Blog Docs GitHub Get Started

Binja: High-Performance Jinja2 Template Engine for Bun

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>"
When to use AOT

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,
})
Performance Tip

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 &lt;script&gt; #}

{# 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
ESC