Bigger than assumed network footprint for LiveView

I build a dash board for an imaginary stock portfolio (Phoenix 1.6). I assumed that LiveView only sends the data which changes but it seems that a lot more is send. Here’s a screenshot:

Am I using LiveView in a wrong way? Shouldn’t the network footprint for updates be much smaller? I know that we are still talking peanuts here. I just want to understand why all @stocks are always updated even when just one element of @stocks has changed and how a possible fix would look like.

Here’s the code:

defmodule DemoWeb.StockWatchLive do
  use DemoWeb, :live_view

  @refresh_rate 2500

  def mount(_params, _session, socket) when is_map_key(socket, :stocks) do
    {:ok, update_world(socket)}

  def mount(_params, _session, socket) do
    if connected?(socket), do: Process.send_after(self(), :tick, @refresh_rate)

    {:ok, initialize_world(socket)}

  def render(assigns) do
        <tr><td>Barguthaben:</td><td><%= @balance %></td></tr>
        <tr><td>Wert des Portfolios:</td><td><%= @portfolio_value %></td></tr>
        <tr><td>Summe:</td><td><%= @balance + @portfolio_value %></td></tr>
          <th colspan="2"></th>
      <%= for {stock_name, value} <- @stocks do %>
          <td><%= stock_name %></td>
          <td><%= value %></td>
          <td><%= Map.get(@portfolio, stock_name) %></td>
          <td width="20%">
            <%= if value <= @balance do %>
              <button phx-click="buy" phx-value-ref={stock_name}>Kaufen!</button>
            <% end %>
          <td width="20%">
            <%= if Map.has_key?(@portfolio, stock_name) && Map.get(@portfolio, stock_name) > 0 do %>
              <button phx-click="sell" phx-value-ref={stock_name}>Verkaufen!</button>
            <% end %>
      <% end %>

  def handle_info(:tick, socket) do
    Process.send_after(self(), :tick, @refresh_rate)

    {:noreply, update_world(socket)}

  def handle_event("buy", %{"ref" => stock_name}, socket) do
    {:noreply, execute_order(stock_name, 1, socket)}

  def handle_event("sell", %{"ref" => stock_name}, socket) do
    {:noreply, execute_order(stock_name, -1, socket)}

  defp stock_price(stocks, stock_name) do
    Map.get(stocks, stock_name)

  defp portfolio_value(portfolio, stocks) do
    for {stock_name, amount} <- portfolio, reduce: 0 do
      running_sum -> running_sum + stock_price(stocks, stock_name) * amount

  defp execute_order(stock_name, amount, socket) do
    %{:stocks => stocks, :portfolio => portfolio, :balance => balance} = socket.assigns

    portfolio = Map.update(portfolio, stock_name, 1, &(&1 + amount))

    balance = balance + -1 * amount * stock_price(stocks, stock_name)

    |> assign(portfolio: portfolio)
    |> assign(portfolio_value: portfolio_value(portfolio, stocks))
    |> assign(balance: balance)

  defp update_world(socket) do
    %{:stocks => stocks, :portfolio => portfolio} = socket.assigns

    stocks =
      for {k, v} <-
          into: %{},
          do: {k, random_fluctuation(v)}

    |> assign(stocks: stocks)
    |> assign(portfolio_value: portfolio_value(portfolio, stocks))

  defp random_fluctuation(value) do
    case Enum.random(1..4) do
      4 -> value + Enum.random(-5..6)
      _ -> value

  defp initialize_world(socket) do
    |> assign(
      stocks: %{
        "Aktie A" => 100,
        "Aktie B" => 200,
        "Aktie C" => 300,
        "Aktie D" => 400
    |> assign(portfolio: %{})
    |> assign(balance: 1_000)
    |> assign(portfolio_value: 0)

On my mobile so hard to see the whole code, but afaik you should not use functions in the render. Those should be done in the assigning function so the diff tracking can do it’s job. See liveview docs about the diffing.

It’s OK to use functions in the render, and even encouraged in the documentation:

# Avoid:
<% some_var = @x + @y %>
<%= some_var %>
# Instead, use a function:
<%= sum(@x, @y) %>

The Pitfalls referred to in the docs are:

  • Non-standard do/end blocks
  • Variable assignment

Neither seem to apply in this case.

Looking at the logs, it doesn’t seem like an abnormally large footprint, though. The assign which is being updated is @stocks - the list of stocks. So it makes sense that each time you update the list, the full list is sent. To avoid this, maybe temporary assigns could be implemented?

I don’t think you have anything to worry about yet. Something you may want to avoid is to have a large complicated data structure, which is almost always changing, in the assign, and to have an accessor function that take a small slice of it, which often don’t change, and use it in the rendering.

The for comprehension to render the table rows is going to re-render and resend all the stock rows when any individual stock(s) change, so that is what you are seeing. You can optimize this case by rendering the table rows as a LiveComponent, which will only produce a diff for the changed stocks.


Thank you! That was the missing piece I was searching for.