Django LiveView vs Phoenix LiveView: a real benchmark | Andros Fenollosa
I was curious: how does Django LiveView hold up against the original Elixir implementation when you compare them under identical conditions? Not with theoretical arguments, but with real numbers.
I built an identical alert dashboard in both frameworks: add, delete, and search alerts in real time. The benchmark is automated with Playwright headless Chromium, measuring the time between the user action and the DOM change, plus the bytes sent over WebSocket per interaction.
The source code is in the repository and is fully reproducible with Docker Compose.
Let's look at the results.
The stack
Component<br>Django LiveView<br>Phoenix LiveView
Language<br>Python 3.12<br>Elixir 1.17.3 / OTP 27
Framework<br>Django 6.0.5<br>Phoenix 1.7
LiveView<br>django-liveview 2.2.0<br>phoenix_live_view 1.0
Server<br>Uvicorn 0.47.0<br>Bandit 1.5
WS layer<br>Channels 4.3.2 + Redis 7<br>BEAM (built-in)
Common scenarios
I ran 10 iterations (2 warmup) for add, delete, and search on a list with a few items.
Scenario<br>Django LiveView avg (ms)<br>Phoenix avg (ms)
Add alert<br>32.18<br>32.26
Delete alert<br>32.29<br>32.24
Search / filter<br>9.77<br>10.13
A complete tie. Both frameworks land in the same range: ~32 ms for mutations and ~10 ms for search. The difference is statistically irrelevant.
Edge cases
This is where things get interesting.
Scenario<br>Django avg (ms)<br>Phoenix avg (ms)
Large list (500 items)<br>60.42<br>53.49
Rapid fire (5 clicks)<br>321.80<br>318.44
Empty search<br>12.39<br>7.84
With 500 alerts loaded, Phoenix is 12% faster. Rapid fire is practically identical.
However, the most striking data point is the payload:
Scenario<br>Django receives<br>Phoenix receives
Add alert<br>5,348 B<br>976 B
Delete alert<br>3,421 B<br>829 B
Large list (500 items)<br>327,428 B<br>66,382 B
Rapid fire (5 clicks)<br>60,953 B<br>12,490 B
With the large list, Django LiveView transfers 327 KB per action versus Phoenix's 66 KB. Every time you add an alert with 500 already loaded, Django LiveView sends you the entire table again. Phoenix only sends the new row.
It's a design difference, not an implementation flaw. In Django LiveView you explicitly define which selector to update and with what HTML:
@liveview_handler("add_alert")<br>def add_alert(consumer, content):<br>alerts = list(Alert.objects.all())<br>html = render_to_string("components/_alerts_table.html", {"alerts": alerts})<br>send(consumer, {"target": "#alerts-container", "html": html})<br>In Phoenix LiveView you update the assigns and the framework computes the diff:
def handle_event("add_alert", _params, socket) do<br>alert = Alerts.create_random_alert()<br>{:noreply, assign(socket, alerts: [alert | socket.assigns.alerts])}<br>end<br>Phoenix knows which part of the template changed and only sends that. Django LiveView has no such mechanism, so granularity is up to you: point to a small selector and you send less, point to a large container and you send everything. Does this matter? On slow connections or with many concurrent clients, yes. In an app with few users or small lists, it will be indistinguishable.
Conclusion
The data answers my original question: yes, Django LiveView holds up against Phoenix LiveView. In day-to-day operations they are practically equal. Phoenix's advantage shows when the payload grows. Even then, it's a problem you can mitigate with careful design in Django LiveView by targeting more specific selectors.
The advantage of Django LiveView is staying in Python and the Django ecosystem without leaving what you already know, and that Django LiveView is more explicit and predictable: you decide what HTML to send and where to place it.
I'm pleasantly surprised.
The stack
Common scenarios
Edge cases
Conclusion
This work is under a Attribution-NonCommercial-NoDerivatives 4.0 International license.
Will you buy me a coffee?
Support me on Ko-fi
Comments
page#run"<br>data-liveview-function="show_comment_email"<br>data-id="80134668"<br>data-type="article"<br>>Leave a comment
There are no comments yet.
You may also like
page#run"<br>data-liveview-function="navigate_article"<br>data-uuid="06892b5b">
python
django
django channels
liveview
django liveview
Performance comparison of Django's main interactive frameworks
page#run"<br>data-liveview-function="navigate_article"<br>data-uuid="7134f59f">
python
django
liveview
Why I forked instead of taking the easy way out
page#run"<br>data-liveview-function="navigate_article"<br>data-uuid="7b1b607b">
python
django
django channels
liveview
django liveview
DOOM in Django: testing the limits of LiveView at 600.000 divs/seconds
Visitors in real time
You are alone: 🐱