Efficient way to show list of users with LiveView?

I’m using LiveView for a chat app. List of users in a room are updated in real-time. Users are stored as a list in socket assigns. The problem is that on each user add/delete the whole list gets sent down to each client - consuming a lot of bandwidh and CPU on the client.

Is there a native LiveView way to only send “diffs” - e.g. only 1 user that needs to be added/deleted from DOM?
(I know about prepend/append options, but it doesn’t work if you want to list all currently active users in a room)

Take a look at this guide, it may have something that works for your case. You should be able to render the initial list using temporary assigns and then send diffs down to append/prepend/remove.

I’ve just started looking at something similar myself and I’m wondering if the use of Phoenix Presence and PubSub would be suitable for your requirements.

1 Like

If I understand this guide correctly, you can use append/prepend options to efficiently handle new content (e.g. chat log), but to display active users you need to be able to not only append, but also delete old entries, and this does not cover it.

BTW I’ve just tried changing user data structure from a list of maps to a map of maps (i.e. instead of [%{id: 1, name: "user1"}, ...] use %{uuid1: %{name: "user1"}, uuid2: ...}, and iterating in the template like this: for {id, user} <- @users}, but the behaviour is the same - the whole list gets sent when 1 user joins/leaves.

This is fine when there are 5 users in a room, but when there are 500 browser tab even crashes sometimes.

If I understand correctly there’s currently no way to handle this use case in LiveView natively, and something like this is better implemented with Channels + custom JS on the client?

I’ve been reading through some docs and I’m thinking you definitely don’t need to step outside of LiveView, but you do need to dig into some of the JS interop. It shouldn’t be too complicated, mind you. Basically you should be able to end up with a hook mounted on your container and then something along the lines of

def handle_info({:user_left, user_id}, socket) do
  {:noreply, socket |> push_event(“user_left”, %{user_id: user_id}}

The hook will be relatively simple — it’ll define a callback to handle the event and then remove the element with the correct id from the dom.

I could’ve sworn there was a way to push LiveView.JS commands directly down a socket but I can’t for the life of me find an example of it now, and I’m not in a position to test.

Just curious, just how many users are we talking about. And how much data each user represent. I have chats with hundreds of people, and the roaster is just a plain list. Not much data to optimize for.

but to display active users you need to be able to not only append, but also delete old entries, and this does not cover it.

You can delete (in a sense). If each user’s element has a id attribute, you can modify them, such as add a class="hidden" to effectively “delete” it.

I’ve noticed performance issues around 500 users, the whole list is about 150kb, sent multiple times per second. Interestingly, I discovered that much (all?) of performance degradation can be attributed to browser developer tools - when I have dev tools open, it quickly eats CPU and RAM, and almost no issues when it’s closed.

So for now I think I’ll leave it as is, and maybe later implement some kind of update rate (once a second?) to “batch” the changes.

This sounds more like an issue with change tracking. Is that a real scenario where users would be joining and leaving constantly several times per second?