How to Optimize CPU Usage in Live View Application when introducing Scrolling Animation

I have a live view application that displays exchange rates data. Within the application, there is a box called “announcement” which shows scheduled or adhoc messages. Sometimes the message consists of around 3 lines, but occasionally it can be longer. However, the message box can only display up to 3 lines. So, when the message exceeds 3 lines, I decided to implement a scrolling animation behavior.

Here’s how I implemented it:

First, I added a Phoenix Hook to listen for when the message box is loaded. If the message box is loaded, I assume that there is a message to display.

Hooks.MessageBox = {
  mounted() {
    updateAnimationForMessage("message", this);
  },
  updated() {
    updateAnimationForMessage("message", this);
  },
};

Then, I added a JavaScript function to check the number of lines in the current message.

  const messageList = document.querySelector("#message-list");
  const content = messageList.querySelector("li:first-child");
  const cssCompute = window.getComputedStyle(content);
  context.pushEvent(event, {
    number_of_lines: Math.floor(
      parseInt(cssCompute.height) / parseInt(cssCompute.lineHeight)
    ),
  });
}

In the LiveView, I created a list with 10 rows of <li>Message here</li> elements using a for loop. These messages are scrolled up using CSS animation set to run infinitely. I push the number of lines count to the LiveView. The LiveView receives the number of lines and adds the CSS animation with inline styling.

["animation: message-slideup #{calculate_message_animation_time(number_of_lines) * 10}s linear infinite"]

I expected this code to work without any unexpected behavior or increased CPU usage. However, when I deployed it to production, I noticed that the CPU usage doubled with 200 clients accessing the application. Therefore, I am seeking help to resolve this issue.

Since this is a purely cosmetic effect, maybe you can fully implement it on the client side without extra roundtrips to the server?

Hooks.MessageBox = {
  mounted() {
    const numberOfLines = calculateNumberOfLines("message", this);
    const animationTime = calculateMessageAnimationTime(numberOfLines);
    this.style.animation = `message-slideup ${animationTime}s linear infinite`;
  },
  updated() {
    // same as mounted()
  },
};

Actually my message content div is inside Surface.LiveComponent. So that live view is updated this view every
30s because I run the genserver to update my message every 30s so message animation is not smooth when I do this on javascript.

<div id="message-content">
  <ul id="message-list">
    <li :for={{ _x <- Utils.get_message_loop(10) }}>{{get_in(@message, [:text])}}</li>
  </ul>
</div>

What is the frequency of updateAnimationForMessage? If it’s running something like requestAnimationFrame and you’re pushing an event from there the event is hitting the server at least 60 times a second (more in high refresh rate monitors), which for 200 users would be 12000 events a second that the server has to handle, which would explain the memory use.

I agree with @ibarch, you probably should figure out a way to decouple the animation from the roundtrips to the server.

If it is only running a single time the problem isn’t in the javascript and we would need more info about your problem to figure out where the bottleneck is.

1 Like

Please note that the code provided is not the complete code; it is only a reference to understand the problem. Thank you.

I have a message box component called Surface LiveView. Here’s an example of how it looks:

defmodule MessageBox do
  property(message, :map, default: %{})

  def render(assigns) do 
  ~H"""
    <div id="message-content">
      <ul id="message-list">
        <li :for={{ _x <- Utils.get_message_loop(10) }}>{{get_in(@message, [:text])}}</li>
      </ul>
    </div>
  """  
  end
end

I have a GenServer that updates the @message every 30 seconds and calls the broadcast function to notify the LiveView about the updated message:

messages = get_message_from_db()
MyWeb.Endpoint.broadcast!(
  "message:" <> tv_id,
  "show_message",
  %{message: messages}
)

In my handle_info function, I simply update the message:

socket =
  socket
  |> assign(:message, message)

The GenServer broadcasts the updated messages every 30 seconds, so the socket assignment will be called every 30 seconds. This means that the LiveView will update the HTML code every 30 seconds.

If I add code to add animation using JavaScript, the Phoenix Hooks function is called when the update happens, which resets my animation if it hasn’t finished yet.

What does messages = get_message_from_db() return? A single message or a list of all previous messages? If it is a list of all previous messages it would make the memory grow pretty fast since each liveview would have a massive @message assign in each liveview, if that’s the case you can look at something like stream to keep them out of the server.

(Even if it returns only the last message, the @message assign will grow each 30s with each client keeping the full list of messages since they connected stored in the server).

Edit: I should not answer when I’m almost asleep, I swear that I thought that the problem was a RAM and not a CPU one, so feel free to ignore it, but CPU usage isn’t exactly a problem as long as the system is still responsive.

I am implementing an automatic scrolling behavior for a that contains a large amount of text. When I add the following function:

function myFunction() {
  const element = document.getElementById("message-content");
  let x = element.scrollLeft;
  let y = element.scrollTop;
  
  element.scrollTop += 1;
  setTimeout(myFunction, 10);
}

The text within the starts shaking while it moves. I am seeking help on how to handle or debug this issue.

This code that I added to my project is exactly same with the below link w3school don’t shaking but mine does.

Try to apply phx-update="ignore" attribute to the parent container and fill it with dummy values on LW mount.
If the scrolling runs smoothly, then it’s likely that Morphdom patching is interfering with your manual DOM updates in the hook. To resolve this issue you can try to configure Morpdom to carry over some properties during patching:

const liveSocket = new LiveSocket("/live", Socket, {
  ...,
  dom: {
    onBeforeElUpdated(fromEl, toEl) {
      if (fromEl.matches(".parent")) {
        // I'm not sure which properties need to be moved from fromEl to toEl
      }
    }
  }
});

If animation jittering remains intact you need to debug your JS implementation.

1 Like