Django LiveView vs Phoenix LiveView: a real benchmark

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 Django LiveView Phoenix LiveView
Language Python 3.12 Elixir 1.17.3 / OTP 27
Framework Django 6.0.5 Phoenix 1.7
LiveView django-liveview 2.2.0 phoenix_live_view 1.0
Server Uvicorn 0.47.0 Bandit 1.5
WS layer Channels 4.3.2 + Redis 7 BEAM (built-in)

Common scenarios

I ran 10 iterations (2 warmup) for add, delete, and search on a list with a few items.

Latency — common scenarios

Scenario Django LiveView avg (ms) Phoenix avg (ms)
Add alert 32.18 32.26
Delete alert 32.29 32.24
Search / filter 9.77 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.

Latency distribution

Edge cases

This is where things get interesting.

Latency — edge case scenarios

Scenario Django avg (ms) Phoenix avg (ms)
Large list (500 items) 60.42 53.49
Rapid fire (5 clicks) 321.80 318.44
Empty search 12.39 7.84

With 500 alerts loaded, Phoenix is 12% faster. Rapid fire is practically identical.

However, the most striking data point is the payload:

Data received per action

Scenario Django receives Phoenix receives
Add alert 5,348 B 976 B
Delete alert 3,421 B 829 B
Large list (500 items) 327,428 B 66,382 B
Rapid fire (5 clicks) 60,953 B 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")
def add_alert(consumer, content):
    alerts = list(Alert.objects.all())
    html = render_to_string("components/_alerts_table.html", {"alerts": alerts})
    send(consumer, {"target": "#alerts-container", "html": html})

In Phoenix LiveView you update the assigns and the framework computes the diff:

def handle_event("add_alert", _params, socket) do
  alert = Alerts.create_random_alert()
  {:noreply, assign(socket, alerts: [alert | socket.assigns.alerts])}
end

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.

PS: You are looking at a site built 100% with Django LiveView, I invite you to push its limits.

This work is under a Attribution-NonCommercial-NoDerivatives 4.0 International license.

Will you buy me a coffee?

Comments

There are no comments yet.

Written by Andros Fenollosa

May 15, 2026

3 min of reading

You may also like

Visitors in real time

You are alone: 🐱