Skipping events in live view

I have a live view app where the user has some analog controls, like sliders.

When the user drags the slider, I send the new slider position to the server, update the socket state and send some response.

I works very well, but I have a small issue, if the users drags the slider back and forth quickly, it will send a lot of events to the phoenix live view, which the elixir code can cope with with no problem, but the JS side is lagging. Even as I am updating a single node in the page, it can take up to 2 seconds for events to “settle down” on the JS side.

So basically the behavior is as follow:

  • user drag slider back and forth, I watch the slider position in js and I use pushEvent in the hook to send the new position
  • server side I do some light state update, I see that the last value has been reached in the server console (if the user generated 100 event, I see in the phoenix console that event 100 has been received and processed)
  • client side, I stopped all interaction, but I still see a series of phx-Fj51eTYOX9gIKG7C update being printed in the console for like 1-2 seconds. Those updates are tiny and update only one html element each, but they still take some time to be processed.

I know I could debounce the pushEvent on the JS side, but the problem is that I need the phoenix state in sync with the client, so I need all events, and phoenix has no problem processing them.

That’s why I am wondering if there is a way to see all incomming events, group them by element id, sort them by date and only apply the last one to the DOM.

Did you try using throttle or debounce? It makes no sense to send more events to the server than it can considerably update things by.

The server can process the events fine, it’s the client the problem, I am investigating now to see what could cause the issue.

The same goes for the client not being able to keep up with updates from the server. And given the slider is what triggers those it might still be worthwhile to debounce or throttle those events.

If you have can’t debounce the client events for whatever reason, you can do event compression on the server.

Store the changes in an assign that does not trigger template changes (which is what all those diffs to the clients would be coming from) and send a message to self that you can process in a handle_info to compress the events. In that handler, process the events captured on the server side down into a single dataset and use that to update the assigns that cause changes in the template.

This should result in a single update to the client after whatever timeout you set for the message sent by the LiveView to itself while still processing all the events from the client.

2 Likes

Event compression server side is a very good idea, I’ll try it.

1 Like

What you want is definitely phx-debounce as there is likely no reason to consume those collective events not the server. Start on the debounce side for sure, ie phx-debounce="500"

This reminds me of similar challenges one has when first starting with React - and how sometimes the explicitness of react state - and how changes trigger re-rendering - might not actually be the behaviour for which the business logic is concerned about I.E the form for which the slide position is consumed/concerned about…

In this slider case - you want good UX when the user moves the slider really quick - which should really just be client side/UI state imo - so it can updated at like 60+ fps no problem, and network latency cannot cause ‘janky’ sliding behaviour. Its only when the user settles on a position of the slider that the backend should care, and the UI push that state to the server…

This might not work depending on what you are doing. For example if the slider once passed a certain threshold, does something else to the UI that relies on server side state - then maybe you do want to send the slider position to the server in ‘real’ time, and have the UI and backend state synchronized… - in that case some kind of optimistic response would likely keep it smooth.

The problem is that what is triggered by the slider requires server state, also I need real time position of the slider to be known to the server.

What I ended up doing is the following:

  • I sent real time events with pushEvent("store"), which do not produce any diff client side
  • I debounce those events client side, and when the debounce times out, I send pushEvent("trigger") to the server which update the template states and trigger a diff

That sounds very much like what I was proposing, good compromise!

Yeah, and it gives me the flexibility to send event to the client from the server if I need to (similar to the compression idea mentioned before).