Reactive server-side rendering for Django, powered by Rust
djust brings Phoenix LiveView-style reactive views to Django. You write server-side Python; the browser updates itself over a WebSocket. There is no JavaScript to write, no bundler, and no build step in your project.
djust.org · Documentation · Quick Start · Examples
from djust import LiveView, event_handler
class CounterView(LiveView):
template_string = """
<div dj-root>
<h1>Count: {{ count }}</h1>
<button dj-click="increment">+</button>
<button dj-click="decrement">-</button>
</div>
"""
def mount(self, request, **kwargs):
self.count = 0
@event_handler
def increment(self):
self.count += 1 # the page updates; no JavaScript
@event_handler
def decrement(self):
self.count -= 1- One codebase. Views, state and event handlers are Python. No API layer, no frontend build.
- Small wire traffic. A Rust virtual DOM diffs each render and sends only the changed patches.
- Fast templates. A Rust template engine renders Django templates 7–11x faster on variable- and filter-heavy pages (Performance).
- Tiny client. ~61 KB gzipped runtime, injected automatically. Nothing to bundle.
- Django all the way down. Your templates, forms, auth, permissions and ORM work as they are, with CSRF, escaping and per-view authorization built in.
- Resilient transport. WebSocket with automatic reconnection and an HTTP fallback.
The fastest start is the scaffold, which configures everything below:
pip install djust
djust new myprojectTo add djust to an existing project instead:
1. Settings. Add the apps and a channel layer to settings.py:
INSTALLED_APPS = [
# ... your existing apps ...
"channels",
"djust",
]
ASGI_APPLICATION = "myproject.asgi.application"
CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}}2. asgi.py. Route WebSockets to djust:
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from djust.websocket import LiveViewConsumer
from django.urls import path
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter([path("ws/live/", LiveViewConsumer.as_asgi())])
),
})3. A view, a URL and a template.
# myapp/views.py
from djust import LiveView, event_handler
class CounterView(LiveView):
template_name = "counter.html"
def mount(self, request, **kwargs):
self.count = 0
@event_handler
def increment(self):
self.count += 1# myproject/urls.py
from django.urls import path
from myapp.views import CounterView
urlpatterns = [path("counter/", CounterView.as_view(), name="counter")]<!-- myapp/templates/counter.html -->
{% load live_tags %}
<!DOCTYPE html>
<html>
<head>
<title>Counter</title>
{% djust_client_config %}
</head>
<body>
<div dj-root>
<h1>Count: {{ count }}</h1>
<button dj-click="increment">+</button>
</div>
</body>
</html>4. Run it with uvicorn myproject.asgi:application and open /counter/.
In DEBUG, djust hot-reloads views without restarting or losing state, so you
don't need --reload.
On each event djust re-renders the view on the server, diffs the result against the previous render in Rust, and sends only the patches.
| In the template | Purpose |
|---|---|
{% djust_client_config %} in <head> |
Emits client config. djust injects the client runtime into every LiveView response; you never add a <script> tag. |
dj-root |
Marks the reactive region. Only HTML inside it is diffed and patched. djust stamps dj-view onto it with the dotted path of the view. |
dj-view="myapp.views.MyView" |
Optional. Write it yourself only to name a specific view, such as an embedded or sticky view, or a template shared by several views. |
dj-click, dj-input, dj-change, dj-submit |
Send events to @event_handler methods. Inputs pass value; forms pass their fields. |
dj-key or data-key on list items |
Gives items a stable identity, so reorders become moves and keep focus, scroll position and animations. |
{% for item in items %}
<div dj-key="{{ item.id }}">{{ item.name }}</div>
{% endfor %}Without a key, lists are diffed by position: still correct, with more DOM
changes on reorders. Conditional attributes such as
class="btn {% if active %}active{% endif %}" are handled correctly too, with
or without {% else %}. See the VDOM architecture guide
and the template cheat sheet.
You don't need LiveView to use the Rust engine. Point a TEMPLATES entry at the
backend, and your existing TemplateViews, render() calls and {% include %}s
render through Rust, with no WebSocket and no client runtime:
TEMPLATES = [
{
"BACKEND": "djust.template_backend.DjustTemplateBackend",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {"context_processors": [...]},
},
{
# admin and contrib templates still need Django's own backend
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {"context_processors": [...]},
},
]- 98.57% of Django's own
template_testssuite passes unmodified against this backend (1032 of the 1,047 cells that reach an engine at all; measured byscripts/run-django-template-suite.pyagainst the Django tag matching the installed version — seedocs/TEMPLATE_BACKEND.mdfor the full breakdown and what the remaining cells are). - Rendering is 7–11x faster on variable- and filter-heavy templates; static markup is not faster, because there is nothing to accelerate.
djust new configures this backend for you.
Full render, same template on both engines, parsed once on each side.
benchmarks/benchmark.py on an Apple silicon laptop, Django 5.2.16, Python
3.12, DEBUG=False, release build:
| Template | Rows | Django | djust | Speedup |
|---|---|---|---|---|
| Static markup | 10,000 | 2.85 ms | 2.81 ms | 1.0x |
| Simple list (2 vars/row) | 10,000 | 63.7 ms | 8.96 ms | 7.1x |
| Filtered list (typical page) | 10,000 | 241 ms | 21.5 ms | 11.2x |
The speedup grows with variable and filter density. The table leaves out the
bigger win on updates, where djust sends a diff and plain Django re-sends the
whole page. Reproduce it with make build && python benchmarks/benchmark.py.
The script refuses a debug build, which is roughly 7.6x slower.
| Topic | Guide |
|---|---|
| Directives, filters and tags | Template cheat sheet |
| Reusable components and theming | Components |
| Tailwind and Bootstrap setup | CSS frameworks |
dj-patch, dj-navigate, live_redirect() |
Navigation |
@debounce, @throttle, @cache, @background |
State management |
| Event handler conventions | Event handlers |
Debug panel (Ctrl/Cmd+Shift+D) |
Debug panel |
| Production with uvicorn, Redis and Nginx | Deployment |
| Building with an AI coding agent | Conventions · AI references |
Everything else is at docs.djust.org. Working examples live in examples/demo_project.
Browser client runtime (~61 KB gz) ── events up, patches down
↕ WebSocket (or HTTP fallback)
Django LiveView classes, event handlers, state (Python, Channels)
↕ PyO3
Rust core template engine · VDOM diff · HTML parser · MessagePack
Building from source needs Rust 1.70+ and uv:
git clone https://github.com/djust-org/djust.git
cd djust
make install # dependencies via uv, then a release build of the Rust core
make test # Python + Rust + JavaScript
make help # everything elseSee CONTRIBUTING.md and the testing guide.
CSRF protection, automatic escaping in the Rust engine, WebSocket origin
validation and session auth, rate limiting, and view- and handler-level
permissions are built in; manage.py djust_audit reports your views' auth
posture. Report vulnerabilities to security@djust.org (see SECURITY.md).
- djust.org · Documentation · Issues · support@djust.org
- Sponsor on GitHub, or star the repo to help others find it.
MIT licensed (LICENSE). Inspired by Phoenix LiveView; built with PyO3 and html5ever.
