4 minutes read

Improve Signalr and React Performance User Experience

With the last two articles, we now have a working app that shows everyone’s cursor in real time. But before we deploy this to production, let’s do some much needed performance optimizations!

Three cursors speeding across a screen

This article is part of my series “Realtime webapps with SignalR and React”:

Too Many Updates per Second

Modern monitors and graphics cards can handle over 60hz, with some even reaching 240hz. This means the cursor moves between 60 to 240 times per second, causing each client to send updates up to 240 times per second over the SignalR connection.

It’s easy to see this doesn’t scale well. We can definitely sacrifice some of the buttery-smooth cursor movement for scalability and performance. For example, Miro allegedly updates just four times per second.

One way to solve this is by throttling our mousemove event listener. Throttling limits how often the event handler runs, ensuring it only executes at set intervals.

Since we might have different throttle requirements in the future, it is best to do this using a higher-order function instead of changing the actual event listener:


_17
// utils.js
_17
const Throttle = (fn, delay) => {
_17
let ready = true;
_17
_17
// return the throttled function
_17
return (...args) => {
_17
if (!ready) {
_17
return; // block the function if it's not ready
_17
}
_17
_17
ready = false; // block the function after this call
_17
fn(...args);
_17
setTimeout(() => {
_17
ready = true; // allow the function to be called again after the delay
_17
}, delay);
_17
};
_17
};

A few JavaScript-specific things are happening here:

  1. The higher-order function takes another function fn and a delay time delay.
  2. A ready variable is set outside the returned function to track when it can run again.
  3. Using an arrow function ensures this is captured correctly, so we can use ready inside the throttled function.
  4. The returned function spreads the current arguments and calls fn(...args).

By using this higher-order function, we can easily adjust the throttle settings without touching the core logic of the event listener.

Finally, we can wrap our updateCursorPosition function in the useUpdateCursorLocation hook with our new Throttle function like this:


_10
//normal event handler
_10
const updateCursorPosition = (ev: MouseEvent) => {
_10
// ...function body
_10
};
_10
_10
//throttled event handler
_10
const updateCursorPosition = Throttle(
_10
(ev: MouseEvent) => {
_10
// ...function body
_10
}, 250);

Improving User Experience with Springs

After applying throttling, you’ll notice the user experience has become quite jarring. Instead of smooth movement, the other cursors seem to jump around the screen at noticeable intervals.

While we can’t update the cursor position 60-240 times per second to recreate the exact movements of the other cursors, we can improve the user experience by transitioning smoothly between each updated position.

I got the best results using springs. Springs are an animation technique that don’t rely on a timeline and predefined curves but instead mimic physical properties like stiffness, mass, and damping. The folks over at liveblocks.io dive deeper into different animations and their pros and cons for multiplayer cursor animation in this article.

There are several libraries that provide spring animations. Two of the most popular are react-spring and framer-motion. Since my projects often need more than just spring animations, I prefer using framer-motion.

Adding framer-motion is straightforward. We simply replace the div in our OtherCursor component with motion.div like this:


_20
// Feel free to change the settings to suit your animation preference
_20
const spring = {
_20
type: "spring",
_20
damping: 30,
_20
mass: 0.8,
_20
stiffness: 350,
_20
}
_20
_20
export function OtherCursor({ color, name, x, y }: OtherCursorProps) {
_20
return (
_20
<motion.div
_20
style={{ background: color, position: "absolute"}}
_20
initial={{ x, y }}
_20
animate={{ x, y }} // framer-motion now handles the position of each cursor
_20
transition={spring}
_20
>
_20
<div>{name}</div>
_20
</motion.div>
_20
);
_20
}

With this, our mouse movement looks a lot smoother. Personally, I still prefer updating the mouse positions more than 4 times per second since a 250ms delay still looks a bit janky. But depending on your use cases, hardware and performance requirements, this approach works well. If Miro can get away with it, you probably can too.

Wrapping up

Last time, in Sync React with SignalR Events, we connected a React app to a .NET SignalR Hub for realtime cursor tracking. While that solution worked, it wasn’t quite production-ready. Having 60 to 240 cursor updates per second from multiple clients is a bit overkill and not even necessary.

In this article we took control of the updates-per-second by implementing a throttle function. With throttling, we can set a millisecond threshold to limit how often a function can fire.

While this solved performance issues, it hurt the user experience. By introducing spring-based animations, we brought back smooth mouse movement and improved the user experience.

That’s it for now, see you in the next one!

What to read next:

I really hope you enjoyed this article. If you did, you might want to check out some of these articles I've written on similar topics.