LiveView append and performance degradation with large number of appended elements

I’m noticing quite a bit of slowdown when appending messages and replacing elements on a LiveView webpage which has an ever-increasing number of chat messages being displayed.

Each incoming message is wrapped in a p tag, and there are multiple spans within the text inside that p tag. As I start getting up to several hundred messages, replacing the input/form with a new one by using a new changeset starts to take some significant time, and the insertion of messages into the window starts creeping up to ~50ms just because of a reflow being triggered. The process of fully handling the form/input replacement can work its way up to 500ms or more.

Using some manual editing, I took those hundreds of messages and modified them so they were all part of a single huge p element on the page. Same number of text characters and ‘lines’, but just in one p tag instead of hundreds. This dropped the time to swap out the input field and insert new messages considerably, to a level which (while noticed) is at least acceptable. If I remove all the messages and have a blank slate, the insertion time is sub 15ms.

Does anyone have any tips/tricks they use with having a large amount of text and doing a lot of appending? Do I need to create some javascript hooks that go in and shuffle around the text LiveView pushes to the page so that each incoming message ends up in a single p element with all the other messages? Should I just accept having even less text available and only allow ~100 messages (this really isn’t workable for me honestly) to live at a time? Should I switch to channels and cut out LiveView dealing with sending messages to and from the client, leaving LiveView to handle other parts of the UI that aren’t chat related?

5 Likes

You may want to take a look to the video from Chris McCord where he builds a Twitter clone and see how he is appending tweets, instead of triggering a full render of all tweets in the page:

Unfortunately he doesn’t provide the link for a Github repo, but if you take a look to the topic Where is the chirp repo for the real-time Twitter clone in 15 minutes by Chris McCord? you will find several links to different repos and discussion about it.

1 Like

I am already using temporary assigns and appending to the DOM. The update happens exactly as in that video with small chunks of html being sent over and the update happening very quick…when there are a few items on the screen like in that demo.

My issue isn’t triggering a rerender of all messages from the perspective of the server, but that slipping these tiny chunks of HTML into the DOM is triggering a browser redraw in some cases and it’s causing huge spikes in apparent lag due to the browser freezing up while it scans the DOM to recalculate stuff.

Now what’s a bit odd is that while replacing the form in place takes a huge amount of time, I can perform a click interaction elsewhere on the same page that inserts and removes elements and it takes almost no time at all. So the redraw being triggered and it taking a while is happening only in a couple cases, apparently based on the relative position of those elements to the huge list of messages I am inserting as that last quick case happens “to the side” of where the messages are displayed.

This may very well be a browser limit thing I can’t get around and I have to get fancy with some tricks like I mentioned in the original post (using JS to shift incoming messages into the existing single “mega message”) but I lose some flexibility that way.

Part of me is almost wondering if I’m going to need to keep the messages server side and allow the user to “scan” through their messages up and down using hotkeys and swapping out what messages I show based on state. Much how many client side JS libraries handle large collections to keep performance good.

1 Like

So I am new to Live View and still in the learning process, but I was wondering if you are rendering each message as a live_component, using socket update instead of assign, and at same time adding phx-update="prepend" to the for loop rendering the messages?

Maybe you could share your code or repo so that the experts here can help you better then me.

1 Like

I’m not rendering a series of live components as I don’t need that complexity for the messages. However, other than that I’m doing almost what I’ve seen done elsewhere. However, rather than injecting values into an html fragment, I instead build up the HTML elsewhere and send it over to LiveView to be injected raw, rather than having values interpolated in. So my message might actually be: "<p>Goodbye <span class="emotion">cruel</span> <span class="place">world!</span><p>

Skipping the rest of the layout because it’s not important, but this is in my client.leex file:

  <div class="w-3/5 flex flex-col">
    <div id="story" phx-hook="Story" phx-update="append" class="border-4 border-r-0 min-w-full flex-1 flex flex-col overflow-y-scroll p-2 font-extrabold font-story">
      <%= for message <- @messages do %>
        <%= raw(message) %>
      <% end %>
    </div>
    <div class="h-8 flex flex-col">
      <%= form_for @input, "#", [phx_submit: :submit_input, class: "flex flex-col flex-1"], fn _f -> %>
        <%= text_input :input, :content, phx_hook: "Input", phx_blur: "stop_typing", placeholder: "Enter Commands Here...", class: "min-w-full rounded-lg resize-y min-h-full", autocomplete: "off" %>
      <% end %>
    </div>
  </div>

The return for my mount:

    {:ok,
     assign(socket,
       character_id: session["character_id"],
       input: Input.new(),
       messages: [],
       client_data: client_data
     ), temporary_assigns: [messages: []]}

Are you sure this is the culprit?
Are you also doing other LiveView stuff?
For example, once I had some component rendering an inspect of some state and it was killing performance.
In the mean time I was investigating the wrong LiveView for problems.

1 Like

It looks like you are defeating the programming model of Live View here that is built to avoid precisely this “complexity”.

From my understanding in a for-loop you will want to use a live_component so that live view can properly track the changes and be able to do its amazing diff to only send the values that have changed to the client, not the html. Live view engine separates static from dynamic parts, but you are defeating that in your code, or if you prefer you are working against the program model of live view.

The culprit is a large number of individual DOM nodes on the webpage causing a very long redraw operation by the browser. ~50ms just to insert a single message into the div containing all of the p elements containing the individual messages.

Appending a message is pretty quick, relatively speaking, but swapping out the form/input by creating a new changeset (which triggers LiveView to send over a new form HTML fragment) takes over 500ms when we’re talking hundreds of messages. This happens because on my webpage the design is that the window containing messages sits directly above the text input field, so replacing the form means the browser takes a look at everything above it and it forces a redraw calculation of the form, the window containing the messages, and the hundreds of messages within the window.

I have a completely different LiveView interaction on the same webpage that happens in another ‘column’, and it’s still quick even with hundreds of messages. So the culprit is absolutely browser redraws/calculations when elements are replaced/added to the page in such a way that things have to be recalculated.

This is why I asked how people are handling having large numbers of messages when using LiveView and appending/updating the page, because that’s where the issue comes from.

To be fair this is also a problem with pure client side libraries and the reason why e.g. many client side frameworks have components, which render just the visible rows of lists instead of every available item.

3 Likes

It looks like you are defeating the programming model of Live View here that is built to avoid precisely this “complexity”.

I think you are misunderstanding LiveView, what it is, and what it is built on. The templating system EEX which LiveView is built on has been around since much earlier Erlang. All of the templates you see, whether the normal EEX templates or the LEEX templates, end up being processed and turned into strings after which is where LiveView comes in.

Using EEX absolutely does make it easier to write things like loops and create complex interfaces, but that has nothing to do with LiveView and is not a functional feature of LiveView at all. LiveView works just fine if you take EEX out of the equation entirely and write everything by hand.

From my understanding in a for-loop you will want to use a live_component so that live view can properly track the changes and be able to do its amazing diff to only send the values that have changed to the client, not the html. Live view engine separates static from dynamic parts, but you are defeating that in your code, or if you prefer you are working against the program model of live view.

For loops have nothing to do with Live Components. A Live Component is simply a way to encapsulate state (in some cases) and display logic in an easily reusable way, much like templates. That demo only used LiveComponents because it was effectively writing a bunch of cards to the webpage, each with like buttons and other interactivity. If you don’t need to encapsulate state, because you’re just writing out some text, you don’t need to use Live Components. That’s a red herring.

LiveView is about replacing JS as much as possible to provide richer UI experiences and to provide for a whole new set of interoperability between client and server out of the box while keeping as much server side as possible to reduce context change, development context switching, and improve the security and integrity of data. All of the templates, for loops, and everything else that makes building the actual HTML easy is outside LiveView’s bailiwick.

Which I called out explicitly in the initial post. And hence my asking, in the first post, how people are approaching it and even gave the idea of simulating that using hotkeys and “scrolling” through the messages that way.

Please consider my post withdrawn.

This is not true at all. You’re correct in that eex and leex templates are processed by their respective view modules, but there’s a big difference in how they’re processed. Only .leex templates (or ~L"…" code) does get detailed change tracking, which allows liveview to send static parts only once and on updates send only the tiniest bits of updated markup instead of large chunks of markup, where 95% are already present on the client. I’m not sure how much, but it should at least help morphdom to make better choices in what to update in the dom.

Plain eex templates will only be compiled to functions returning iodata – no change tracking for parts of the template. Which is why you’d want to be careful to use .leex everywhere liveview is involved.

You’re correct, but if I understand liveview correctly live components also act as kind of a “root” element for updates on the client side / for morphdom. So it could be possible that using components might improve the client side performance as well.

Sorry, didn’t read the whole topic thoroughly. But this can be a solution. If you have to much stuff, reducing the amount of stuff is one way to resolve issues. You can do this either by using client side rendering and keeping data on the client, but rendering only parts (liveview can e.g. work nicely with alpine.js) or by doing the same on the server with more latency.

I’m not sure this is a great description of liveview. Liveview is a great tool to reduce complexity in places where server and client need to interact anyways. It’s a suboptimal solution for any interaction, which doesn’t need to involve the server. It’s also not inherently more secure than comparable js solutions.

3 Likes

I was having a problem similar to yours, in my case, I have a panel that preloads 40 thousand cards (in the most extreme case), distributed in 7 lists (this is a Kanban style project that I am doing). However, in a more friendly way, I limited each list to appearing only the first 20. And that already starts to return me some delays the moment I execute some action of events, like for example, opening the card modal, creating a tag for the card, editing, moving etc. This delay had a duration of ~ 60ms. Due to the LiveComponents feature that they offer, I chose to share several LiveComponents throughout my project, by the type of communication and properties. Now with this change, I can get an improvement in performance on my panel, due to the behavior of divisions of the HTML bits, but from time to time there are some short delays, which do not get in the way of UX, because it was a very extreme case in which I would carry 40 thousand cards.

2 Likes

Plain eex templates will only be compiled to functions returning iodata – no change tracking for parts of the template. Which is why you’d want to be careful to use .leex everywhere liveview is involved.

You are right in that how they are processed is very different, but my understanding is that the templates enter the processing code as strings. How you interpolate values into those strings doesn’t matter from the point of view of LiveView. As far as I understand it, whether I do <div class="<%= "foo" %>"> or <%= raw(foo) %> where the variable foo is "<div class=\"foo\">" doesn’t really matter as you end up with the same html with the same values will be in the string that is then processed by the LiveView code.

Am I misunderstanding the sequence of events?

You’re correct, but if I understand liveview correctly live components also act as kind of a “root” element for updates on the client side / for morphdom. So it could be possible that using components might improve the client side performance as well.

I can see how that might help in some cases. Would obviously depend heavily on how it impacts redrawing. I’ll take a peek at that.

I’m not sure this is a great description of liveview.

I’m not happy with it either, but LiveView is certainly not at all about making writing the actual html for a page easier which was my main point.

It’s also not inherently more secure than comparable js solutions.

Perhaps I should have said it’s easier to be more secure. Because you’re right, you can make JS solutions secure…at the expense of doubling up all your logic and doing the validation checks server side.

1 Like

How, exactly, did you implement your components? For example, did you just replace a form element in a LEEX template with a call to render a live component, which did nothing more than render that same form element that was originally created in the main template? In such a case I would love to understand how that would lead to an improvement as it seems like you would still have the same DOM manipulation issues.

Did you also change the overall structure of the page as you also implemented the components?

It seems you are. LiveView does not process the resulting markup after assigns are interpolated. It only processes the templates. .leex templates are processed, so that static parts of the template are taken apart from dynamic parts. The dynamic parts also have dependency tracking, which allows to update only the parts, which actually depend on the data being updated.

<div class="<%= "foo" %>">

becomes something akin to (pseudo-code)

%{static: ["<div class=\"", 1, "\">"], dynamic: %{1 => %{depends_on: fn -> @foo end}}

where ["<div class=\"", 1, "\">"] can be sent once to the client and only the value of @foo is sent on updates.
… while

<%= raw(foo) %>

would be represented like this:

%{static: [1], dynamic: %{1 => %{depends_on: fn -> raw(@foo) end}}

where the whole raw(@foo) would be sent on each update.

If you now render a .leex template you don’t get back a plain string, but a struct, holding those details, where the runtime can now decide to send only the parts that matter to the client, significantly reducing the amount of traffic on the wire (imagine the above nested a few handful of layers).

Non .leex templates essentially result in the same unoptimized sending as the latter example.

6 Likes

Components would solve your issues in this case. The issue is we have to patch the entire LiveView DOM container for any change, and for large DOM trees, this requires traversing the tree as you alluded to. If you wrap the paragraphs in a component and/or the form in a component, it will allow LiveView to patch only those areas when they change, and also skip walking those areas when the parent LiveView needs to patch a DOM a non-component node that it owns.

11 Likes

Alright then, I guess I’m gonna go write a couple of components and see what impact it has on the performance.

1 Like

Thank you for the explanation.

1 Like