smoothness is latency.
A remote cursor is the simplest real-time feature you can build: read a position, send it, draw it. The naive version still feels broken. The other cursor stutters and teleports, and more bandwidth doesn’t fix it. The fix is to render the past on purpose.
The naive version
Send every pointermove, draw every arrival. Both halves
are wrong.
On the send side, pointermove fires at display refresh
rate: 60 to 120 events a second from one hand. Everyone receives
everyone else’s stream, so a room of ten is fanning out
thousands of messages a second whose content is mostly “the
cursor moved half a pixel.”
On the receive side, the network delivers those messages late, unevenly spaced, and sometimes out of order. A cursor drawn at arrival time replays that noise as motion.
The send side
Throttle on a clock and coalesce: every 50ms, send the latest position and discard the backlog. Sequence-number each message so a late arrival can’t overwrite a newer one; last write wins. Twenty messages a second sounds coarse; smoothed on the receiving end, it’s indistinguishable from a hundred at a fifth of the fan-out.
The receive side
Arrival times can’t be repaired, so stop rendering at arrival time. The fix, borrowed from multiplayer games, is entity interpolation: buffer incoming snapshots and render the cursor at a fixed offset in the past, around 100ms, interpolating between the two snapshots that bracket that moment. Jitter disappears into the buffer, and the motion is interpolated between real samples: continuous by construction.
Feel it
The panel below runs your own cursor through a simulated network — the sliders are the network, the toggles are the fixes. Nothing is staged: messages really are delayed, jittered, and dropped when stale, and the counters are measured live.
The tradeoff
Interpolation is bought with delay: the remote cursor runs about 100ms behind the hand that moves it. In my experience the trade is worth it — a steady 100ms reads as presence, a jittery cursor reads as broken, even at a lower average delay. Size the buffer from your jitter distribution; a buffer smaller than the spikes just moves the stutter around.
The principle
The newest data makes the worst motion. Real-time rendering is a scheduling problem: commit to a render time the network can reliably meet, then hold it.