Binja
High-performance multi-engine template system for Bun. Jinja2/DTL, Handlebars & Liquid support. 2-4x faster than Nunjucks, 160x with AOT. 84 filters, template inheritance, autoescape, debug panel.
Install / Use
/learn @egeominotti/BinjaREADME
Why binja?
| Feature | Binja | Other JS engines | |---------|-----------|------------------| | Runtime Performance | ✅ 2-4x faster | ❌ | | AOT Compilation | ✅ 160x faster | ❌ | | Multi-Engine | ✅ Jinja2, Handlebars, Liquid, Twig | ❌ | | Framework Adapters | ✅ Hono, Elysia | ❌ | | Django DTL Compatible | ✅ 100% | ❌ Partial | | Jinja2 Compatible | ✅ Full | ⚠️ Limited | | Template Inheritance | ✅ | ⚠️ | | 84 Built-in Filters | ✅ | ❌ | | 28 Built-in Tests | ✅ | ❌ | | Debug Panel | ✅ | ❌ | | CLI Tool | ✅ | ⚠️ | | Autoescape by Default | ✅ | ❌ | | TypeScript | ✅ Native | ⚠️ | | Bun Optimized | ✅ | ❌ |
Benchmarks
Tested on Mac Studio M1 Max, Bun 1.3.5.
Two Rendering Modes
| Mode | Function | Best For | vs Nunjucks |
|------|----------|----------|-------------|
| Runtime | render() | Development | 2-4x faster |
| AOT | compile() | Production | 160x faster |
Runtime Performance (vs Nunjucks)
| Benchmark | binja | Nunjucks | Speedup | |-----------|-------|----------|---------| | Simple Template | 371K ops/s | 96K ops/s | 3.9x | | Complex Template | 44K ops/s | 23K ops/s | 2.0x | | Multiple Filters | 246K ops/s | 63K ops/s | 3.9x | | Nested Loops | 76K ops/s | 26K ops/s | 3.0x | | Conditionals | 84K ops/s | 25K ops/s | 3.4x | | HTML Escaping | 985K ops/s | 242K ops/s | 4.1x | | Large Dataset | 9.6K ops/s | 6.6K ops/s | 1.5x |
AOT Compilation (Maximum Performance)
| Benchmark | binja AOT | binja Runtime | Speedup | |-----------|-----------|---------------|---------| | Simple Template | 14.3M ops/s | 371K ops/s | 39x | | Complex Template | 1.07M ops/s | 44K ops/s | 24x | | Nested Loops | 1.75M ops/s | 76K ops/s | 23x |
Installation
bun add binja
Quick Start
import { render } from 'binja'
// Simple rendering
const html = await render('Hello, {{ name }}!', { name: 'World' })
// Output: Hello, World!
// With filters
const html = await render('{{ title|upper|truncatechars:20 }}', {
title: 'Welcome to our amazing website'
})
// Output: WELCOME TO OUR AMAZI...
Using Environment
import { Environment } from 'binja'
const env = new Environment({
templates: './templates', // Template directory
autoescape: true, // XSS protection (default: true)
})
// Load and render template file
const html = await env.render('pages/home.html', {
user: { name: 'John', email: 'john@example.com' },
items: ['Apple', 'Banana', 'Cherry']
})
AOT Compilation (Maximum Performance)
For production, use compile() for 160x faster rendering:
import { compile } from 'binja'
// Compile once at startup
const renderUser = compile('<h1>{{ name|upper }}</h1>')
// Use many times (sync, extremely fast!)
const html = renderUser({ name: 'john' })
// Output: <h1>JOHN</h1>
Production example:
import { compile } from 'binja'
// Pre-compile all templates at server startup
const templates = {
home: compile(await Bun.file('./views/home.html').text()),
user: compile(await Bun.file('./views/user.html').text()),
}
// Rendering is now synchronous and extremely fast
app.get('/', () => templates.home({ title: 'Welcome' }))
app.get('/user/:id', ({ params }) => templates.user({ id: params.id }))
Features
Variables
{{ user.name }}
{{ user.email|lower }}
{{ items.0 }}
{{ data['key'] }}
Conditionals
{% if user.is_admin %}
<span class="badge">Admin</span>
{% elif user.is_staff %}
<span class="badge">Staff</span>
{% else %}
<span class="badge">User</span>
{% endif %}
Loops
{% for item in items %}
<div class="{{ loop.first ? 'first' : '' }}">
{{ loop.index }}. {{ item.name }}
</div>
{% empty %}
<p>No items found.</p>
{% endfor %}
Loop Variables
| Variable | Description |
|----------|-------------|
| loop.index / forloop.counter | Current iteration (1-indexed) |
| loop.index0 / forloop.counter0 | Current iteration (0-indexed) |
| loop.first / forloop.first | True if first iteration |
| loop.last / forloop.last | True if last iteration |
| loop.length / forloop.length | Total number of items |
| loop.parent / forloop.parentloop | Parent loop context |
Template Inheritance
base.html
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Default Title{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
page.html
{% extends "base.html" %}
{% block title %}My Page{% endblock %}
{% block content %}
<h1>Welcome!</h1>
<p>This is my page content.</p>
{% endblock %}
Include
{% include "components/header.html" %}
{% include "components/card.html" with title="Hello" %}
Set Variables
{% set greeting = "Hello, " ~ user.name %}
{{ greeting }}
{% with total = price * quantity %}
Total: ${{ total }}
{% endwith %}
Filters (84 Built-in)
binja includes 84 built-in filters covering both Jinja2 and Django Template Language.
String Filters (26)
| Filter | Description | Example |
|--------|-------------|---------|
| upper | Uppercase | {{ "hello"\|upper }} → HELLO |
| lower | Lowercase | {{ "HELLO"\|lower }} → hello |
| capitalize | First letter uppercase | {{ "hello"\|capitalize }} → Hello |
| capfirst | First char uppercase | {{ "hello"\|capfirst }} → Hello |
| title | Title case | {{ "hello world"\|title }} → Hello World |
| trim | Strip whitespace | {{ " hi "\|trim }} → hi |
| striptags | Remove HTML tags | {{ "<p>Hi</p>"\|striptags }} → Hi |
| slugify | URL-friendly slug | {{ "Hello World!"\|slugify }} → hello-world |
| truncatechars | Truncate to N chars | {{ "hello"\|truncatechars:3 }} → hel... |
| truncatewords | Truncate to N words | {{ "a b c d"\|truncatewords:2 }} → a b... |
| truncatechars_html | Truncate preserving HTML | {{ "<b>hi</b> world"\|truncatechars_html:5 }} |
| truncatewords_html | Truncate words in HTML | {{ "<p>a b c</p>"\|truncatewords_html:2 }} |
| wordcount | Count words | {{ "hello world"\|wordcount }} → 2 |
| wordwrap | Wrap at N chars | {{ text\|wordwrap:40 }} |
| center | Center in N chars | {{ "hi"\|center:10 }} → hi |
| ljust | Left justify | {{ "hi"\|ljust:10 }} → hi |
| rjust | Right justify | {{ "hi"\|rjust:10 }} → hi |
| cut | Remove substring | {{ "hello"\|cut:"l" }} → heo |
| replace | Replace substring | {{ "hello"\|replace:"l","x" }} → hexxo |
| indent | Indent lines | {{ text\|indent:4 }} |
| linebreaks | Newlines to <p>/<br> | {{ text\|linebreaks }} |
| linebreaksbr | Newlines to <br> | {{ text\|linebreaksbr }} |
| linenumbers | Add line numbers | {{ code\|linenumbers }} |
| addslashes | Escape quotes | {{ "it's"\|addslashes }} → it\'s |
| format | sprintf-style format | {{ "Hi %s"\|format:name }} |
| stringformat | Python % format | {{ 5\|stringformat:"03d" }} → 005 |
Number Filters (9)
| Filter | Description | Example |
|--------|-------------|---------|
| abs | Absolute value | {{ -5\|abs }} → 5 |
| int | Convert to integer | {{ "42"\|int }} → 42 |
| float | Convert to float | {{ "3.14"\|float }} → 3.14 |
| round | Round number | {{ 3.7\|round }} → 4 |
| add | Add number | {{ 5\|add:3 }} → 8 |
| divisibleby | Check divisibility | {{ 10\|divisibleby:2 }} → true |
| floatformat | Format decimal places | {{ 3.14159\|floatformat:2 }} → 3.14 |
| filesizeformat | Human file size | {{ 1048576\|filesizeformat }} → 1.0 MB |
| get_digit | Get Nth digit | {{ 12345\|get_digit:2 }} → 4 |
List/Array Filters (22)
| Filter | Description | Example |
|--------|-------------|---------|
| length | List length | {{ items\|length }} → 3 |
| length_is | Check length | {{ items\|length_is:3 }} → true |
| first | First item | {{ items\|first }} |
| last | Last item | {{ items\|last }} |
| join | Join with separator | {{ items\|join:", " }} → a, b, c |
| slice | Slice list | {{ items\|slice:":2" }} |
| reverse | Reverse list | {{ items\|reverse }} |
| sort | Sort list | {{ items\|sort }} |
| unique | Remove duplicates | {{ items\|unique }} |
| batch | Group into batches | `{{ items|batc
