Jeden stos zamiast dwóch - dlaczego Elixir + Phoenix LiveView wygrywa z Django + React długoterminowo
Marek, CTO startupu fintech, miał zespół 4 osób: 2 backendowców w Pythonie i 2 frontendowców w Reacie. Po 18 miesiącach rozwoju produktu zorientował się, że prosta zmiana typu "dodaj pole do formularza i zapisz w bazie" wymaga dotknięcia 8-10 plików w 4 różnych miejscach.
Model Django. Serializer. Viewset. URL routing. Komponent React. Stan lokalny. Wywołanie API. Obsługa błędu.
I to tylko prosta zmiana.
Kiedy Marek przeanalizował, jak wygląda praca zespołu, wyszło mu coś niepokojącego: 4 osoby w dwóch specjalizacjach produkują tyle samo funkcji, co 2 osoby full-stack.
Problem dwóch stosów
Architektura "backend API + frontend SPA" była odpowiedzią na konkretne problemy 2013 roku: jQuery spaghetti, brak frameworków, SEO wymagające renderingu po stronie serwera.
Dziś, w 2026 roku, ta architektura jest często anty-wzorcem.
Gdzie uciekają roboczogodziny
Zespół z dwoma stosami traci czas na:
| Aktywność | Django + React | Phoenix LiveView |
|---|---|---|
| Synchronizacja typów Python ↔ TypeScript | Ciągła | Nie istnieje |
| Dokumentacja i wersjonowanie API | Wymagane | Nie istnieje |
| Debugowanie "frontend czy backend?" | Częste | Nie istnieje |
| Code review w dwóch językach | Podwójne | Pojedyncze |
| Spotkania synchronizacyjne backend↔frontend | Regularne | Rzadsze |
| Onboarding w dwóch stackach | 4-6 tygodni | 2-3 tygodnie |
Prawdziwy koszt "prostej zmiany"
Dodanie pola phone_number do formularza rejestracji:
| Krok | Django + React | Phoenix LiveView |
|---|---|---|
| Migracja bazy | 1 plik | 1 plik |
| Model | 1 plik | 0 plików (schema) |
| Serializer | 1 plik | - |
| Viewset | 0-1 plik | - |
| API test | 1 plik | - |
| Typ TypeScript | 1 plik | - |
| Komponent React | 1-2 pliki | - |
| Stan lokalny | 1 plik | - |
| Wywołanie API | 1 plik | - |
| Obsługa błędu | 1 plik | - |
| LiveView component | - | 1 plik |
Razem: 8-10 plików vs 2 pliki.
Czas implementacji: 2-4h vs 30-60 minut.
To nie jest kwestia "lepszych programistów". To jest złożoność strukturalna, której nie da się wyeliminować lepszymi praktykami.
Równanie produktywności: 4 = 2
Tu dochodzimy do sedna. Dlaczego 4 osoby w Django + React produkują tyle samo co 2 osoby w Elixir?
Rozkład czasu w zespole 4-osobowym (2 backend + 2 frontend)
| Aktywność | Backend (2 osoby) | Frontend (2 osoby) | Razem |
|---|---|---|---|
| Kod produkcyjny | 60% | 60% | 60% |
| Boilerplate API | 15% | 10% | 12,5% |
| Komunikacja backend↔frontend | 10% | 15% | 12,5% |
| Debugowanie przez sieć | 5% | 10% | 7,5% |
| Code review cross-stack | 10% | 5% | 7,5% |
Tylko 60% czasu to kod, który bezpośrednio rozwiązuje problemy biznesowe.
Rozkład czasu w zespole 2-osobowym (Elixir full-stack)
| Aktywność | Czas |
|---|---|
| Kod produkcyjny | 85% |
| Boilerplate | 5% |
| Komunikacja | 5% |
| Debugowanie | 2,5% |
| Code review | 2,5% |
85% czasu to kod produkcyjny.
Matematyka
Zespół 4 osoby × 160h × 60% = 384h produkcyjnych miesięcznie.
Zespół 2 osoby × 160h × 85% = 272h produkcyjnych miesięcznie.
Różnica: 112h. Ale:
- Mniejszy zespół = krótsze spotkania (zysk 8-16h/mies)
- Jeden stack = mniej błędów (zysk 16-24h/mies na poprawki)
- Pełna własność kodu = mniejsze bus factor (zysk w długim terminie)
W praktyce: 2 osoby w Elixirze dowożą tyle samo funkcji co 4 osoby w Django+React.
Phoenix LiveView - jeden stos, jedna logika
Phoenix LiveView zmienia równanie. Zamiast:
Backend (Django) → REST API → JSON → Frontend (React) → Stan → UIMasz:
Phoenix → LiveView → UILiveView renderuje HTML na serwerze, wysyła diff przez WebSocket, a przeglądarka aktualizuje DOM. Zero API. Zero synchronizacji. Zero duplikacji logiki.
Jak to wygląda w kodzie
Formularz z walidacją w LiveView:
def render(assigns) do
~H"""
<.simple_form for={@form} phx-submit="save">
<.input field={@form[:email]} type="email" label="Email" />
<.input field={@form[:phone]} type="tel" label="Telefon" />
<:actions>
<.button>Zapisz</.button>
</:actions>
</.simple_form>
"""
end
def handle_event("save", %{"user" => params}, socket) do
case Users.register(params) do
{:ok, user} ->
{:noreply, push_navigate(socket, to: ~p"/users/#{user.id}")}
{:error, changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
endTo jest cały kod. Jedna funkcja renderuje formularz, jedna funkcja obsługuje submit. Walidacja jest w changesecie Ecto, błędy wyświetlają się automatycznie.
Ekwiwalent w Django + React
Backend (3 pliki):
# models.py
class User(models.Model):
email = models.EmailField(unique=True)
phone = models.CharField(max_length=20)
# serializers.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['email', 'phone']
# views.py
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializerFrontend (3+ pliki):
// types.ts
interface User {
email: string;
phone: string;
}
// UserForm.tsx
export function UserForm() {
const [user, setUser] = useState<Partial<User>>({});
const [errors, setErrors] = useState<Record<string, string[]>>({});
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setLoading(true);
try {
await api.post('/users/', user);
navigate('/users');
} catch (error) {
if (error.response?.status === 400) {
setErrors(error.response.data);
}
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<Input
label="Email"
value={user.email || ''}
onChange={(e) => setUser({ ...user, email: e.target.value })}
error={errors.email?.[0]}
/>
<Input
label="Telefon"
value={user.phone || ''}
onChange={(e) => setUser({ ...user, phone: e.target.value })}
error={errors.phone?.[0]}
/>
<Button loading={loading}>Zapisz</Button>
</form>
);
}I to jest uproszczona wersja. Brakuje: biblioteki formularzy (react-hook-form), walidacji frontendowej (zod/yup), obsługi optimistic updates, testów E2E.
Dlaczego BEAM zmienia równanie
Phoenix LiveView działa na maszynie wirtualnej BEAM. To nie jest "tylko" framework - to inny model programowania.
Stan po stronie serwera bez problemów
Tradycyjny stan po stronie serwera = sesje, sticky sessions, problemy ze skalowaniem.
BEAM ma procesy per połączenie. Każdy użytkownik ma swój proces LiveView. 100 000 połączeń = 100 000 procesów, każdy zużywa ~2KB pamięci.
# To jest bezpieczne nawet przy 100 000+ użytkownikach
def handle_event("typing", %{"text" => text}, socket) do
# Stan jest w procesie, nie w bazie ani w Redis
# Każdy użytkownik ma izolowany stan
{:noreply, assign(socket, draft: text)}
endW Django musiałbyś:
- Zapisywać draft w sesji (problemy ze skalowaniem)
- Zapisywać w Redis (dodatkowa infrastruktura)
- Zapisywać w bazie (narzut I/O)
W LiveView: stan jest w pamięci procesu, automatycznie garbage-collected.
Hot Code Reload w produkcji
BEAM pozwala aktualizować kod bez przerywania działania. Żadne połączenia nie są zrywane.
# Wdrożenie w piątek 17:00? Możliwe.
# Użytkownicy kontynuują pracę, kod zmienia się pod nimi.W Django + React:
- Restart serwera = zerwane połączenia
- Rebuild frontendu = minuty niedostępności
- Rolling deployment = złożona konfiguracja
Fault tolerance wbudowana
Jeśli proces się wysypie, supervisor go restartuje. Automatycznie. Bez Kubernetes, bez konfiguracji.
children = [
{Phoenix.PubSub, name: MyApp.PubSub},
MyAppWeb.Endpoint,
# Każdy LiveView jest nadzorowany
# Błąd w jednym nie wywraca całej aplikacji
]
# "Let it crash" - filozofia BEAM
# Proces pada, supervisor go restartuje, system żyjeW Django: błąd w wątku = crash całego workera. Potrzebujesz Gunicorn, Supervisor, systemd, Kubernetes...
Tabela porównawcza techniczna
| Aspekt | Django + REST + React | Phoenix + LiveView |
|---|---|---|
| Liczba języków | 2 (Python + TypeScript) | 1 (Elixir) |
| Liczba repozytoriów | 2 (lub monorepo) | 1 |
| API do utrzymania | Tak (DRF/GraphQL) | Nie |
| Duplikacja typów | Tak (Python + TS) | Nie |
| Stan aplikacji | Klient + serwer | Serwer (WebSocket) |
| SEO | Wymaga SSR/Next.js | Out-of-the-box |
| Time to first paint | Wolniejszy (JS bundle) | Szybszy (HTML) |
| Hot reload w produkcji | Nie | Tak (hot code swap) |
| Fault tolerance | Zależna od infrastruktury | Wbudowana (supervisors) |
| WebSockets | Osobna konfiguracja | Wbudowane |
| Real-time | Dodatkowa warstwa | Natywne (PubSub) |
| Bundle JS | 50-200 KB | ~10 KB |
| Testy E2E | Selenium/Playwright (async) | LiveViewTest (synchroniczne!) |
| Procesy per użytkownik | Nie | Tak (~2KB każdy) |
Testowanie: jedna z największych różnic
Testy w Django + React:
# Backend test (pytest)
def test_create_user(api_client):
response = api_client.post('/api/users/', {'email': 'test@example.com'})
assert response.status_code == 201// Frontend test (Jest + Testing Library)
test('submits form', async () => {
render(<UserForm />);
await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.click(screen.getByText(/zapisz/i));
await waitFor(() => expect(mockNavigate).toHaveBeenCalled());
});Testy w LiveView:
# JEDEN test, wszystko synchroniczne
test "creates user", %{conn: conn} do
{:ok, view, _html} = live(conn, "/users/new")
view
|> element("form")
|> render_change(user: %{email: "test@example.com"})
{:ok, _, html} = view |> element("form") |> render_submit()
assert html =~ "Użytkownik utworzony"
endBrak mockowania API. Brak asynchroniczności. Pełna pokrycie backend+frontend w jednym teście.
Real-time: gdzie różnica jest drastyczna
Django + React: dodanie WebSocket
1. Zainstaluj channels + Redis
2. Skonfiguruj ASGI
3. Stwórz Consumer
4. Skonfiguruj routing
5. Dodaj redis do docker-compose
6. Frontend: połącz z WebSocket
7. Obsługa reconnection
8. Obsługa błędów
9. TestyCzas: 8-16h
Phoenix LiveView: real-time out-of-the-box
def handle_info({:new_message, message}, socket) do
{:noreply, update(socket, :messages, fn msgs -> [message | msgs] end)}
end
# I to wszystko. PubSub jest wbudowany.Czas: 30 minut
"Ale React ma większy ekosystem!"
Prawda. npm ma 2 miliony paczek. Hex ma 18 tysięcy.
Ale liczy się to, czego potrzebujesz.
Porównanie pokrycia potrzeb
| Potrzeba | React ecosystem | Elixir ecosystem |
|---|---|---|
| Routing | react-router | phoenix_router (wbudowany) |
| State management | Redux, Zustand, Jotai, Recoil... | assigns (wbudowany) |
| Forms | react-hook-form, Formik, Final Form | Phoenix.HTML.Form (wbudowany) |
| Validation | Zod, Yup, Joi | Ecto.Changeset (wbudowany) |
| HTTP client | axios, fetch, ky | Req |
| WebSockets | socket.io, ws | Phoenix.PubSub (wbudowany) |
| Authentication | Auth0, Clerk, NextAuth | phx_gen_auth (generator) |
| Background jobs | BullMQ, Agenda | Oban |
| Real-time updates | Pusher, Ably | Phoenix.Channels (wbudowany) |
| Monitoring | Sentry, LogRocket | LiveDashboard (wbudowany) |
W ekosystemie Elixira jedna biblioteka często zastępuje 3-4 z npm. I wiele z nich jest wbudowanych w framework.
"Ale nie znajdę programistów Elixira!"
Rozbijmy to na czynniki pierwsze.
Produktywność vs dostępność
| Czynnik | React | Elixir |
|---|---|---|
| Ofert pracy (Polska) | ~3000 | ~150 |
| Kandydatów na ofertę | 15-20 | 3-5 |
| Czas znalezienia seniora | 2-4 tygodnie | 4-8 tygodni |
| Produktywność jednego developera | Bazowa | ~2x (full-stack) |
Dłuższe znalezienie, ale 2 Elixir developerów = 4 React + Python developerów.
Full-stack z definicji
W Django + React:
- Backendowiec: "nie znam Reacta, nie ruszę frontend"
- Frontendowiec: "nie znam Django, nie ruszę backend"
W Phoenix LiveView:
- Każdy programista Elixira może zrobić całą funkcję end-to-end
- Code review w jednym języku
- Bus factor drastycznie spada
Onboarding programisty Python
Elixir ma składnię podobną do Ruby/Python. Programista Python uczy się podstaw w 1-2 tygodnie:
# Python
def calculate_total(items):
return sum(item["price"] * item["quantity"] for item in items)
# Elixir
def calculate_total(items) do
Enum.reduce(items, 0, fn item, acc ->
acc + item.price * item.quantity
end)
endPattern matching i procesy to nowość, ale do podstawowej pracy w Phoenix nie są wymagane od pierwszego dnia.
Kiedy NIE wybralibyśmy LiveView
Uczciwie - LiveView nie jest odpowiedzią na wszystko.
Aplikacje ciężko interaktywne po stronie klienta
Gry w przeglądarce, edytory grafiki, DAW. Tu każda interakcja wymaga round-trip do serwera, co wprowadza latencję. React (lub Vanilla JS + Canvas/WebGL) wygrywa.
Offline-first
LiveView wymaga połączenia. Jeśli aplikacja musi działać bez internetu - React + Service Worker + IndexedDB (lub ElectricSQL z Phoenix.Sync).
Zespół z głęboką ekspertyzą React
Masz 10 seniorów Reacta z 5-letnim doświadczeniem? Migracja będzie kosztowna. Lepiej zoptymalizować istniejący stack.
Legacy aplikacja
Przepisywanie działającego systemu "bo tak ładniej" to anty-wzorzec. LiveView świetnie sprawdza się w nowych projektach i stopniowej migracji (możesz mieć LiveView obok tradycyjnego frontendu).
Podsumowanie
| Czynnik | Django + React (4 osoby) | Phoenix LiveView (2 osoby) |
|---|---|---|
| Kod produkcyjny | 60% czasu | 85% czasu |
| Pliki na prostą zmianę | 8-10 | 2 |
| Czas na prostą zmianę | 2-4h | 30-60 min |
| Testy E2E | Asynchroniczne, mocki | Synchroniczne, bez mocków |
| Real-time | 8-16h setup | 30 min |
| API do utrzymania | Tak | Nie |
| Bus factor | Wyższy (2 specjalizacje) | Niższy (full-stack) |
| Onboarding | 4-6 tygodni | 2-3 tygodnie |
Reguła: im dłużej projekt żyje, tym bardziej jeden stos wygrywa nad dwoma.
Jeśli stoisz przed decyzją o wyborze stosu dla nowego projektu lub rozważasz uproszczenie istniejącej architektury, porozmawiajmy - przeanalizujemy Twój konkretny przypadek i pokażemy, czy i jak Phoenix LiveView może zwiększyć produktywność Twojego zespołu.