Реалізація Live Cursors у мобільному додатку
Розробники часто недооцінюють складність live cursors: здається, що достатньо відправляти координати по WebSocket і відмальовувати їх. Але на практиці виникає безліч нюансів: затримка мережі, різні роздільності екранів, масштабування, велика кількість користувачів. Помилка в будь-якому з цих пунктів робить курсори неюзабельними. Ми впроваджуємо live cursors з нуля або покращуємо наявну реалізацію. У цій статті розберемо ключові технічні рішення на основі нашого досвіду: вибір протоколу (Y.js Awareness або кастомний WebSocket), нормалізацію координат, інтерполяцію анімації та масштабування при 50+ учасниках. Ви дізнаєтесь, як уникнути типових помилок і досягти плавності на мобільних пристроях.
Вибір протоколу: Y.js Awareness або кастомний WebSocket
Y.js Awareness Protocol — оптимальне рішення, якщо в додатку вже використовується Y.js для синхронізації контенту. Awareness зберігає ephemeral-стани: вони не персистять, не входять в історію змін, автоматично видаляються при відключенні користувача.
// Оновлюємо позицію свого курсора
provider.awareness.setLocalStateField('cursor', {
x: normalizedX, // в координатах документа, не екрана
y: normalizedY,
timestamp: Date.now()
});
// Підписуємося на зміни чужих курсорів
provider.awareness.on('change', ({ updated }) => {
updated.forEach(clientId => {
if (clientId === provider.awareness.clientID) return;
const state = provider.awareness.getStates().get(clientId);
if (state?.cursor) {
updateRemoteCursor(clientId, state.cursor);
}
});
});
Якщо Y.js не використовується — кастомний WebSocket-канал із throttle 30ms (≈33fps). Частіші оновлення не дають помітного покращення UX, але збільшують трафік. Серверна сторона може виконувати throttling до 50ms на клієнта для економії ресурсів.
| Характеристика | Y.js Awareness | Кастомний WebSocket |
|---|---|---|
| Складність | Низька (вбудовано) | Середня (свій протокол) |
| Персистентність | Немає | Опціонально |
| Масштабування | До 100+ | Обмежено реалізацією |
| Сумісність | Тільки з Y.js | Будь-який бекенд |
Як нормалізація координат вирішує проблему смикання?
Критичний момент: координати курсора потрібно передавати в системі координат документа, не екрана. У різних користувачів різні zoom-рівні, розміри екрана, положення scroll. Якщо передавати screen coordinates — курсори будуть стрибати по екрану замість плавного слідування за реальною позицією. Наприклад, при zoom 200% в одного користувача та 50% в іншого, одні й ті самі екранні координати відповідають різним точкам документа.
Формула перетворення: cursorX = (screenX + scrollX) / scale. На прийомі зворотно: displayX = documentX * scale - scrollX. При зміні zoom у отримувача позиція курсора автоматично перераховується, що виключає розсинхрон.
Як забезпечити плавність при затримці мережі?
Raw-позиції від сервера — ступінчасті, особливо при затримці 100–200ms. Потрібна інтерполяція.
У React Native з react-native-reanimated:
const remoteCursorX = useSharedValue(0);
const remoteCursorY = useSharedValue(0);
// При отриманні нової позиції від сервера
const updateCursor = (x, y) => {
remoteCursorX.value = withSpring(x, { damping: 20, stiffness: 300 });
remoteCursorY.value = withSpring(y, { damping: 20, stiffness: 300 });
};
const animStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: remoteCursorX.value },
{ translateY: remoteCursorY.value },
]
}));
withSpring додає пружинну інтерполяцію — курсор «наздоганяє» реальну позицію плавно. Альтернатива: withTiming із duration: 80 — простіше, менш «живе».
На Flutter: AnimationController + Tween<Offset> із CurvedAnimation(curve: Curves.easeOut).
На нативному iOS: UIViewPropertyAnimator з .interruptible option — дозволяє переривати та перезапускати анімацію при нових позиціях без артефактів.
| Параметр | withSpring (RN) | withTiming (RN) | AnimationController (Flutter) |
|---|---|---|---|
| Відчуття | Пружинна, жива | Плавна, лінійна | Кастомна через криві |
| Налаштування | damping, stiffness | duration, easing | Curve, duration |
| Продуктивність | Висока (napi) | Висока | Середня (залежить від кривої) |
Чому Canvas-рендеринг кращий за 50 окремих View?
При 50+ користувачах рендеринг кожного курсора як окремого View створює 50+ в'юшок, кожна зі своїм потоком анімації. Це навантажує GPU та збільшує споживання пам'яті. Canvas-based рендеринг малює всі курсори за один прохід в одному контексті. На Flutter використовуємо CustomPainter, на React Native — react-native-skia або Canvas з expo-gl. Тести показують зниження навантаження на CPU до 40% при 100 курсорах порівняно з окремими View.
Масштабування: 50+ користувачів
При великій кількості користувачів кілька проблем:
Трафік. N користувачів × 33fps × ~50 байт = при 50 користувачах ~82 KB/s лише на cursor updates. Рішення: server-side throttling (сервер не пересилає updates частіше ніж раз на 50ms на клієнта) + відключення курсорів для користувачів за межами viewport.
Рендеринг. 50 анімованих в'юшок одночасно на мобілці — навантаження. Використовуємо Canvas-based рендеринг замість окремих View для кожного курсора. Малюємо всі курсори в одному CustomPainter / SKCanvas / Canvas за один прохід.
Ідентифікація. При 50 користувачах ім'я під курсором нечитабельне. Показуємо ім'я лише при hover/tap на курсор, решту часу — лише кольорова точка з аватаром.
Відображення імені та аватара
Ім'я користувача поруч із курсором — класичний UX. Реалізація: floating label, яка слідує за курсором із невеликим offset. Проблема: при русі до краю екрана label виходить за bounds. Потрібен clamp — якщо курсор ближче ніж X px до правого краю, label відображається зліва від курсора.
Аватар замість стандартного вказівника — часто краще, ніж кольорова стрілка. Кругле зображення 24px діаметром, кешоване в пам'яті.
Типові помилки при інтеграції Live Cursors
- Відсутність throttle: відправлення позицій на кожен піксель — надмірно. 30 мс — оптимум.
- Ігнорування координат документа: курсори "розбігаються" при різних zoom.
- Відмальовка курсорів як окремі View при великій кількості: гальмує UI.
- Необробка від'єднання користувача: "мертві" курсори залишаються на екрані.
Покроковий план інтеграції
- Налаштування WebSocket-каналу (Y.js Awareness або кастомний).
- Реалізація нормалізації координат у системі документа.
- Інтерполяція анімації з налаштуваннями під конкретну платформу.
- Оптимізація рендерингу (Canvas-підхід при >20 курсорах).
- Тестування під навантаженням (до 200 одночасних курсорів).
Що входить у нашу роботу
- Аудит поточної архітектури та вибір протоколу (Y.js Awareness / кастомний WebSocket).
- Реалізація на вашому стеку: iOS (Swift + Combine), Android (Kotlin + Compose), Flutter, React Native.
- Інтерполяція анімації з налаштуваннями під конкретні платформи.
- Масштабування до 100+ користувачів із Canvas-рендерингом.
- Тестування під навантаженням (до 200 одночасних курсорів).
- Документація та передача коду, навчання команди.
Наш досвід: 5 років розробки колаборативних мобільних додатків, понад 20 проєктів із real-time синхронізацією. Гарантуємо коректну роботу на всіх цільових пристроях та версіях ОС.
Строки та вартість
Орієнтовні строки: від 1 тижня на прототип до 3 тижнів на повноцінну інтеграцію з навантажувальним тестуванням. Вартість розраховується індивідуально — залежить від складності стеку та необхідної масштабованості. Зв'яжіться з нами для аудиту вашого проєкту. Замовте впровадження Live Cursors під ключ — отримайте консультацію інженера протягом дня.







