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 + ReactPhoenix LiveView
Synchronizacja typów Python ↔ TypeScriptCiągłaNie istnieje
Dokumentacja i wersjonowanie APIWymaganeNie istnieje
Debugowanie "frontend czy backend?"CzęsteNie istnieje
Code review w dwóch językachPodwójnePojedyncze
Spotkania synchronizacyjne backend↔frontendRegularneRzadsze
Onboarding w dwóch stackach4-6 tygodni2-3 tygodnie

Prawdziwy koszt "prostej zmiany"

Dodanie pola phone_number do formularza rejestracji:

KrokDjango + ReactPhoenix LiveView
Migracja bazy1 plik1 plik
Model1 plik0 plików (schema)
Serializer1 plik-
Viewset0-1 plik-
API test1 plik-
Typ TypeScript1 plik-
Komponent React1-2 pliki-
Stan lokalny1 plik-
Wywołanie API1 plik-
Obsługa błędu1 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 produkcyjny60%60%60%
Boilerplate API15%10%12,5%
Komunikacja backend↔frontend10%15%12,5%
Debugowanie przez sieć5%10%7,5%
Code review cross-stack10%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 produkcyjny85%
Boilerplate5%
Komunikacja5%
Debugowanie2,5%
Code review2,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 → UI

Masz:

Phoenix → LiveView → UI

LiveView 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
end

To 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 = UserSerializer

Frontend (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)}
end

W Django musiałbyś:

  1. Zapisywać draft w sesji (problemy ze skalowaniem)
  2. Zapisywać w Redis (dodatkowa infrastruktura)
  3. 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 żyje

W Django: błąd w wątku = crash całego workera. Potrzebujesz Gunicorn, Supervisor, systemd, Kubernetes...


Tabela porównawcza techniczna

AspektDjango + REST + ReactPhoenix + LiveView
Liczba języków2 (Python + TypeScript)1 (Elixir)
Liczba repozytoriów2 (lub monorepo)1
API do utrzymaniaTak (DRF/GraphQL)Nie
Duplikacja typówTak (Python + TS)Nie
Stan aplikacjiKlient + serwerSerwer (WebSocket)
SEOWymaga SSR/Next.jsOut-of-the-box
Time to first paintWolniejszy (JS bundle)Szybszy (HTML)
Hot reload w produkcjiNieTak (hot code swap)
Fault toleranceZależna od infrastrukturyWbudowana (supervisors)
WebSocketsOsobna konfiguracjaWbudowane
Real-timeDodatkowa warstwaNatywne (PubSub)
Bundle JS50-200 KB~10 KB
Testy E2ESelenium/Playwright (async)LiveViewTest (synchroniczne!)
Procesy per użytkownikNieTak (~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"
end

Brak 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. Testy

Czas: 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

PotrzebaReact ecosystemElixir ecosystem
Routingreact-routerphoenix_router (wbudowany)
State managementRedux, Zustand, Jotai, Recoil...assigns (wbudowany)
Formsreact-hook-form, Formik, Final FormPhoenix.HTML.Form (wbudowany)
ValidationZod, Yup, JoiEcto.Changeset (wbudowany)
HTTP clientaxios, fetch, kyReq
WebSocketssocket.io, wsPhoenix.PubSub (wbudowany)
AuthenticationAuth0, Clerk, NextAuthphx_gen_auth (generator)
Background jobsBullMQ, AgendaOban
Real-time updatesPusher, AblyPhoenix.Channels (wbudowany)
MonitoringSentry, LogRocketLiveDashboard (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ść

CzynnikReactElixir
Ofert pracy (Polska)~3000~150
Kandydatów na ofertę15-203-5
Czas znalezienia seniora2-4 tygodnie4-8 tygodni
Produktywność jednego developeraBazowa~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)
end

Pattern 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

CzynnikDjango + React (4 osoby)Phoenix LiveView (2 osoby)
Kod produkcyjny60% czasu85% czasu
Pliki na prostą zmianę8-102
Czas na prostą zmianę2-4h30-60 min
Testy E2EAsynchroniczne, mockiSynchroniczne, bez mocków
Real-time8-16h setup30 min
API do utrzymaniaTakNie
Bus factorWyższy (2 specjalizacje)Niższy (full-stack)
Onboarding4-6 tygodni2-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.