Реализация Cursor Presence (курсоры пользователей) на сайте
Представьте: десять редакторов правят один документ, а курсоры других участников дёргаются, исчезают или накладываются друг на друга. Типичная картина в проектах без грамотной реализации. Cursor presence — отображение курсоров и выделений в реальном времени — кажется простой задачей, но подводных камней хватает: эффективная передача координат, плавная анимация, корректный маппинг при скролле и отключении. Без оптимизации трафик может достигать 10 000 сообщений в секунду при 50 пользователях, что критично для мобильных сетей и увеличивает затраты на сервер до 50% (около 5000 руб./мес. на 10 пользователей). Мы — команда с пятилетним опытом в real-time коллаборации — внедрили cursor presence для 30+ проектов (редакторы, доски, чаты). Предлагаем готовое решение под ключ: за 1–2 дня мы встроим присутствие пользователей с оптимизацией трафика и гарантированной плавностью. Свяжитесь с нами, чтобы получить консультацию и демо.
Throttle и его влияние на трафик
Событие mousemove генерирует 50–100 кадров в секунду. Если отправлять каждое на сервер, при 10 пользователях трафик достигает тысяч сообщений в секунду. Оптимальное ограничение — 30 fps (33 мс). Используйте throttle-функцию — например, из lodash-es:
import { throttle } from 'lodash-es';
const sendCursor = throttle((x: number, y: number) => {
socket.emit('cursor:move', { x, y });
}, 33);
document.addEventListener('mousemove', (e) => {
sendCursor(e.clientX, e.clientY);
});
document.addEventListener('mouseleave', () => {
socket.emit('cursor:leave');
});
На сервере (Socket.IO) — broadcast в комнату, исключая отправителя:
socket.on('cursor:move', (data: { x: number; y: number }) => {
socket.to(roomId).emit('cursor:update', {
userId: socket.data.userId,
...data,
});
});
socket.on('cursor:leave', () => {
socket.to(roomId).emit('cursor:remove', {
userId: socket.data.userId,
});
});
socket.on('disconnect', () => {
socket.to(roomId).emit('cursor:remove', {
userId: socket.data.userId,
});
});
Экономия трафика при 30 fps вместо 60 составляет до 50% — это критично для мобильных сетей.
Расчёт экономии трафика
При 50 сообщениях в секунду на пользователя и 10 пользователях, трафик составляет 500 сообщений/с. Throttle до 30 fps снижает до 300 сообщений/с. Экономия 40% — примерно 2000–3000 руб./мес. при текущих тарифах.Почему lerp-интерполяция обязательна?
Прямое обновление позиции по каждому событию вызывает рывки. CSS-переход transition: transform 0.1s linear создаёт задержку в обратную сторону. Единственно правильное решение — linear interpolation (lerp) в requestAnimationFrame:
interface RemoteCursor {
userId: string;
name: string;
color: string;
current: { x: number; y: number };
target: { x: number; y: number };
el: HTMLElement;
}
const cursors = new Map<string, RemoteCursor>();
function lerp(a: number, b: number, t: number) {
return a + (b - a) * t;
}
function animateCursors() {
cursors.forEach((cursor) => {
cursor.current.x = lerp(cursor.current.x, cursor.target.x, 0.35);
cursor.current.y = lerp(cursor.current.y, cursor.target.y, 0.35);
cursor.el.style.transform =
`translate(${cursor.current.x}px, ${cursor.current.y}px)`;
});
requestAnimationFrame(animateCursors);
}
animateCursors();
// При получении обновления — только меняем target
socket.on('cursor:update', ({ userId, x, y, name, color }) => {
if (!cursors.has(userId)) {
const el = createCursorElement(userId, name, color);
document.body.appendChild(el);
cursors.set(userId, {
userId, name, color,
current: { x, y },
target: { x, y },
el,
});
} else {
cursors.get(userId)!.target = { x, y };
}
});
Коэффициент интерполяции 0.35 даёт плавное движение без заметной задержки.
Сравнение подходов: WebSocket vs Yjs Awareness
| Критерий | Чистый WebSocket | Yjs Awareness |
|---|---|---|
| Время внедрения | 1–2 дня с нуля | Полдня, если Yjs уже используется |
| Нагрузка на сервер | Выше (своя логика broadcast) | Ниже (встроенный broadcast) |
| Сложность расширения | Средняя (добавление полей вручную) | Низкая (автоматическое состояние) |
| Поддержка выделения текста | Требует дополнительной логики | Встроенная через Awareness |
Yjs Awareness внедряется в 3 раза быстрее, чем чистый WebSocket, если Yjs уже используется. Стандарт WebSocket (см. RFC 6455) определяет протокол двусторонней связи.
Как выбрать между WebSocket и Yjs Awareness?
Если в проекте уже используется Yjs — используйте Awareness: он автоматически синхронизирует состояние курсоров, выделение текста и другие поля. В противном случае достаточно чистого WebSocket. Для справки: WebSocket — стандартный протокол, а Yjs — CRDT-библиотека для совместного редактирования.
Процесс работы
- Аналитика — определяем сценарии использования, нагрузку, текущий стек.
- Проектирование — выбираем протокол (WebSocket или Yjs), проектируем модель данных.
- Реализация — пишем клиент-серверную часть, подключаем throttle, lerp, TTL.
- Тестирование — проверяем при 50+ одновременных пользователях, замеряем трафик.
- Деплой и поддержка — разворачиваем на вашем хостинге, даём гарантию 30 дней.
Что делать, если курсоры исчезают при скролле?
Храните координаты относительно документа (clientX + scrollOffset), а при рендере вычитайте текущий scroll. Иначе чужие курсоры будут смещаться при прокрутке.
document.addEventListener('mousemove', throttle((e) => {
provider.awareness.setLocalStateField('cursor', {
x: e.clientX + window.scrollX,
y: e.clientY + window.scrollY,
});
}, 33));
function getCursorViewportPos(docX: number, docY: number) {
return {
x: docX - window.scrollX,
y: docY - window.scrollY,
};
}
TTL и очистка зависших курсоров
Если пользователь закрыл вкладку без явного disconnect, курсор остаётся на экране. Решение — heartbeat + TTL на клиенте:
const CURSOR_TTL = 5000; // 5 секунд без обновлений
const lastSeen = new Map<string, number>();
socket.on('cursor:update', ({ userId, ...pos }) => {
lastSeen.set(userId, Date.now());
updateCursor(userId, pos);
});
setInterval(() => {
const now = Date.now();
lastSeen.forEach((ts, userId) => {
if (now - ts > CURSOR_TTL) {
removeCursor(userId);
lastSeen.delete(userId);
}
});
}, 1000);
Сравнение вариантов throttle
| Частота (fps) | Трафик на 10 пользователей (сообщений/сек) | Плавность |
|---|---|---|
| 60 | 600 | Идеальная |
| 30 | 300 | Достаточная |
| 15 | 150 | Заметные рывки |
Для большинства задач 30 fps — оптимальный баланс.
Что входит в работу?
При заказе реализации cursor presence вы получаете:
- Архитектуру передачи данных (WebSocket или Yjs)
- Клиентский код с throttle и lerp-интерполяцией
- Обработку координат (document-relative, scroll)
- TTL-механизм для удаления «зависших» курсоров
- Интеграцию с существующей системой (редактор, чат и т.д.)
- Документацию и обучение команды
Реализация cursor presence с нуля занимает 1–2 дня. Если уже используется Yjs или Socket.IO — полдня. Свяжитесь с нами для консультации: оценим ваш проект и предложим оптимальное решение. За пять лет мы выполнили более 30 интеграций — ваш проект будет в надёжных руках. Закажите реализацию сегодня и получите бесплатное тестирование.







