5 min read 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:

utils.js
1
/​/​ utils.​js
2
const Throttle = (fn, delay) => {
3
let ready = true;
4
5
/​/​ return the throttled function
6
return (.​.​.args) => {
7
if (!ready) {
8
return; /​/​ block the function if it's not ready
9
}
10
11
ready = false; /​/​ block the function after this call
12
fn(.​.​.args);
13
setTimeout(() => {
14
ready = true; /​/​ allow the function to be called again after the delay
15
}, delay);
16
};
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:

1
/​/​normal event handler
2
const updateCursorPosition = (ev: MouseEvent) => {
3
/​/​ .​.​.​function body
4
};
5
6
/​/​throttled event handler
7
const updateCursorPosition = Throttle(
8
(ev: MouseEvent) => {
9
/​/​ .​.​.​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:

1
/​/​ Feel free to change the settings to suit your animation preference
2
const spring = {
3
type: "spring",
4
damping: 30,
5
mass: 0.​8,
6
stiffness: 350,
7
}
8
9
export function OtherCursor({ color, name, x, y }: OtherCursorProps) {
10
return (
11
<motion.​div
12
style={{ background: color, position: "absolute"}}
13
initial={{ x, y }}
14
animate={{ x, y }} /​/​ framer-​motion now handles the position of each cursor
15
transition={spring}
16
>
17
<div>{name}</div>
18
</motion.​div>
19
);
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.