In my app, I have a transactions list backed by a stream of 100-500 elements. I’ve noticed that when I trigger an action that causes a new DOM element to be inserted above the transactions list in the DOM, this can be very slow. However, inserting below the list of transactions is fast. This has nothing to do with the server or what’s sent over the wire as I’m not updating the stream at all and the diffs are quite small.
From my reading, I’ve gathered that this might be a result of:
- Inserting a DOM element causes reflow/layout calculations for elements that follow.
- Morphdom may have to do additional book-keeping when inserting elements earlier in the page (unsure about this one).
Interested to hear if others have encountered such issues and whether this is LV + Morphdom related or not.
I’ve attached a minimal repro that streams 10k elements to the page and then allows toggling a header and footer. Toggling the header is slow while toggling the footer is fast. I’m using 10k elements to demonstrate the perf differences and to make up for the fact that the DOM is much simpler in this example than it is for my app.
Application.put_env(:sample, Example.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 5001],
server: true,
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64)
)
Mix.install([
{:plug_cowboy, "~> 2.5"},
{:jason, "~> 1.0"},
{:phoenix, "~> 1.7"},
# please test your issue using the latest version of LV from GitHub!
{:phoenix_live_view,
github: "phoenixframework/phoenix_live_view", branch: "main", override: true}
])
defmodule Example.ErrorView do
def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end
defmodule Example.HomeLive do
use Phoenix.LiveView, layout: {__MODULE__, :live}
def mount(_params, _session, socket) do
items =
Stream.repeatedly(fn ->
%{
id: "item-#{System.unique_integer()}",
date: Enum.random(Date.range(~D[0000-01-01], Date.utc_today())),
account: Enum.random(["Chase", "Bank of America", "Citi Bank"]),
amount: Enum.random(0..1000),
merchant: Enum.random(["Amazon", "Google", "Facebook"])
}
end)
|> Enum.take(10000)
{:ok, socket |> assign(show_header: false, show_footer: false) |> stream(:items, items)}
end
def render("live.html", assigns) do
~H"""
<script src="/assets/phoenix/phoenix.js">
</script>
<script src="/assets/phoenix_live_view/phoenix_live_view.js">
</script>
<script>
let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket)
liveSocket.connect()
</script>
<style>
* { font-size: 1.1em; }
.row { padding: 2px; }
</style>
{@inner_content}
"""
end
def render(assigns) do
~H"""
<button phx-click="toggle_header">toggle header</button>
<button phx-click="toggle_footer">toggle footer</button>
<div :if={@show_header}>header</div>
<hr />
<div id="items" phx-update="stream">
<div id={dom_id} :for={{dom_id, item} <- @streams.items} class="row">
<div>Date: {item.date}</div>
<div>Account: {item.account}</div>
<div>Merchant: {item.merchant}</div>
<div>Amount: {item.amount}</div>
<hr />
</div>
</div>
<div :if={@show_footer}>footer</div>
"""
end
def handle_event("toggle_header", _params, socket) do
{:noreply, assign(socket, show_header: not socket.assigns.show_header)}
end
def handle_event("toggle_footer", _params, socket) do
{:noreply, assign(socket, show_footer: not socket.assigns.show_footer)}
end
end
defmodule Example.Router do
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug(:accepts, ["html"])
end
scope "/", Example do
pipe_through(:browser)
live("/", HomeLive, :index)
end
end
defmodule Example.Endpoint do
use Phoenix.Endpoint, otp_app: :sample
socket("/live", Phoenix.LiveView.Socket)
plug(Plug.Static, from: {:phoenix, "priv/static"}, at: "/assets/phoenix")
plug(Plug.Static, from: {:phoenix_live_view, "priv/static"}, at: "/assets/phoenix_live_view")
plug(Example.Router)
end
{:ok, _} = Supervisor.start_link([Example.Endpoint], strategy: :one_for_one)
Process.sleep(:infinity)