Что такое 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.