Phoenix (Elixir)MiddleTechnical

Что такое LiveView streams (представленные в Phoenix 0.18+) и почему они эффективнее списков?

LiveView streams хранят коллекцию в DOM браузера, а не в assigns сервера. При изменении сервер отправляет только операцию (insert/delete) над одним элементом, а не весь список — это экономит память и снижает объём передаваемых данных.

LiveView Streams: эффективный рендеринг коллекций

Streams появились в Phoenix LiveView 0.18 (вышел в составе Phoenix 1.7) и решают конкретную проблему: как рендерить и обновлять большие коллекции без хранения всего списка в памяти LiveView-процесса и без полного ре-рендеринга при каждом изменении.

Проблема обычных assigns-списков

# Плохой подход для больших списков
def mount(_params, _session, socket) do
  {:ok, assign(socket, messages: Messages.list_all())}  # 10000 записей в RAM
end

def handle_event("new_message", %{"body" => body}, socket) do
  {:ok, msg} = Messages.create(body)
  # Весь список пересобирается и diff-ится целиком
  {:noreply, update(socket, :messages, &[msg | &1])}
end

При каждом обновлении LiveView вычисляет diff между старым и новым списком. Для 1000 элементов это дорого: и по CPU, и по памяти.

Streams: как работает

Stream хранит элементы коллекции в DOM браузера, а не в assigns LiveView-процесса. Сервер отправляет только операции: «вставь элемент», «удали элемент», «обнови элемент» — без полной коллекции в памяти.

defmodule MyAppWeb.MessageLive do
  use MyAppWeb, :live_view

  alias MyApp.Chat

  @impl true
  def mount(_params, _session, socket) do
    messages = Chat.list_recent_messages(limit: 50)

    socket =
      socket
      |> stream(:messages, messages)
      # Опционально: ограничить DOM до N элементов
      |> stream(:messages, messages, limit: 100)

    if connected?(socket) do
      Phoenix.PubSub.subscribe(MyApp.PubSub, "chat")
    end

    {:ok, socket}
  end

  @impl true
  def handle_info({:new_message, message}, socket) do
    # Вставить в начало DOM; сервер не хранит список
    {:noreply, stream_insert(socket, :messages, message, at: 0)}
  end

  @impl true
  def handle_event("delete", %{"id" => id}, socket) do
    message = Chat.get_message!(id)
    {:ok, _} = Chat.delete_message(message)
    {:noreply, stream_delete(socket, :messages, message)}
  end
end

Шаблон со stream

def render(assigns) do
  ~H"""
  <ul id="messages" phx-update="stream">
    <li
      :for={{dom_id, message} <- @streams.messages}
      id={dom_id}
    >
      {message.body}
      <button phx-click="delete" phx-value-id={message.id}>
        Delete
      </button>
    </li>
  </ul>
  """
end

Ключевые детали: атрибут phx-update="stream" на контейнере и обязательный id={dom_id} на каждом элементе — LiveView использует эти ID для точечных обновлений DOM.

API streams

  • stream(socket, name, items) — инициализация стрима.
  • stream_insert(socket, name, item, at: 0) — вставить в начало (at: -1 — в конец).
  • stream_delete(socket, name, item) — удалить по ID.
  • stream_delete_by_dom_id(socket, name, dom_id) — удалить по DOM ID напрямую.
  • stream(socket, name, [], reset: true) — сбросить стрим и вставить новые элементы.

Почему это эффективнее списков

  • Память процесса: список из 10 000 элементов не хранится в assigns — только в DOM браузера.
  • Diff: при вставке одного элемента сервер отправляет только этот элемент, а не весь список.
  • DOM: браузер обновляет только один <li>, а не перерисовывает весь список.

Подводные камни

  • Структуры в стриме должны реализовывать Phoenix.Param или иметь поле id — иначе LiveView не сможет сгенерировать dom_id.
  • phx-update="stream" обязателен на контейнере — без него LiveView не знает, что это стрим.
  • Нельзя читать элементы стрима из assigns на сервере — они живут только в DOM клиента.
  • При использовании limit: старые элементы удаляются из DOM автоматически, но этот лимит не связан с пагинацией в БД — нужно контролировать separately.
  • stream_insert с уже существующим ID обновляет элемент (upsert-семантика) — это удобно, но может быть неожиданным.
  • reset: true пересылает все элементы заново — не используйте для частичного обновления.
  • Стримы несовместимы с phx-update="append" — это другой механизм.
  • При вложенных стримах (стрим внутри LiveComponent) нужно явно указывать идентификатор компонента.

Common mistakes

  • Сводить liveview streams к названию метода без lifecycle и failure path.
  • Игнорировать модель runtime: Phoenix 1.8 работает поверх Plug, Endpoint, Router, Controllers/LiveViews, PubSub и OTP supervision.
  • Не отделять validation, authorization, transaction boundary и business logic.
  • Менять похожие API местами без учёта семантики ошибок и ownership.

What the interviewer is testing

  • Объясняет liveview streams через конкретную точку lifecycle в Phoenix (Elixir).
  • Приводит корректный минимальный пример без вымышленных методов или callbacks.
  • Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.

Sources

Related topics