Представьте: 50 аналитиков одновременно правят квартальный отчёт в CRM, а изменения пропадают или конфликтуют. Знакомая боль уходит, когда внедряете CRDT — математический алгоритм, гарантирующий детерминированное слияние изменений. Проблема в том, что Google Docs не встраивается в вашу систему, а попытки написать свой коллаборативный редактор с нуля упираются в сложность синхронизации. Мы решаем эту задачу с помощью CRDT-библиотек — Yjs или Liveblocks. Выполнили более 50 проектов по внедрению Yjs за 10+ лет.
Недавно финтех-стартап обратился к нам с похожей ситуацией: требовалась интеграция совместного редактирования документов в собственную CRM. Выбрали Yjs — open-source CRDT, синхронизирующий изменения менее чем за 100 мс. Результат: отсутствие vendor-lock, полный контроль над данными и экономия до 70% на инфраструктуре по сравнению с managed-решениями. Ниже — как повторить этот успех. Если у вас аналогичная задача, свяжитесь с нами для предварительной оценки.
Выбор подхода: Yjs vs Liveblocks
| Критерий | Yjs + Hocuspocus | Liveblocks |
|---|---|---|
| Инфраструктура | Свой сервер (веб-сокеты) | Managed, без инфраструктуры |
| Стоимость | Бесплатно, только хостинг | Плата за активных пользователей |
| Гибкость | Полный контроль | Ограниченный провайдером |
| Сложность | Средняя | Низкая |
| Персистентность | Любая БД (PostgreSQL, MongoDB) | Liveblocks Storage |
Yjs выигрывает по гибкости и отсутствию vendor-lock. Если важен полный контроль и вы готовы поддерживать сервер — это лучший вариант. Liveblocks подходит для быстрого старта без DevOps-забот.
Почему стоит выбирать Yjs для совместного редактирования?
Yjs использует CRDT — алгоритм, гарантирующий детерминированное слияние изменений без центрального координатора. Даже при офлайн-редактировании двух пользователей конфликты разрешаются автоматически. Под капотом — библиотеки для работы с текстом, массивами, картами.
Например, один из наших клиентов (финтех-стартап) нуждался в одновременной работе до 50 аналитиков над квартальным отчётом. После внедрения Yjs с Hocuspocus сервером получили стабильную синхронизацию с задержкой менее 100 мс. В тестах подтвердили поддержку до 500 одновременных пользователей и скорость обработки 2000 изменений в секунду.
Как разрешаются конфликты?
CRDT не требует блокировок. Каждое изменение (операция) получает уникальный идентификатор на основе временной метки и ID узла. При слиянии алгоритм упорядочивает операции так, что результат одинаков на всех участниках. Технически это реализовано через логарифмическую структуру — слияние занимает O(log n).
Архитектура решения: наш кейс
Серверная часть — на Node.js с Hocuspocus. Подключаем базу данных (PostgreSQL) для персистентности состояний Yjs. Аутентификация через JWT, авторизация на уровне документов.
Клиент — React с Tiptap редактором. Используем расширения Collaboration и CollaborationCursor для курсоров других пользователей. Аватары участников отображаются в реальном времени через WebSocket.
Пример кода развернут ниже. Код проверен в продакшене.
// Сервер — Hocuspocus import { Server } from '@hocuspocus/server'; import { Database } from '@hocuspocus/extension-database'; import { Logger } from '@hocuspocus/extension-logger'; const server = Server.configure({ port: 1234, extensions: [ new Logger(), new Database({ fetch: async ({ documentName }) => { const doc = await documentRepo.findByName(documentName); return doc?.content ?? null; }, store: async ({ documentName, state }) => { await documentRepo.upsert(documentName, state); } }) ], async onAuthenticate({ token, documentName }) { const payload = jwt.verify(token, process.env.JWT_SECRET); const canAccess = await checkDocumentAccess(payload.sub, documentName); if (!canAccess) throw new Error('Access denied'); return { userId: payload.sub }; } }); server.listen(); // Клиент — React + Tiptap import { useEditor, EditorContent } from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; import Collaboration from '@tiptap/extension-collaboration'; import CollaborationCursor from '@tiptap/extension-collaboration-cursor'; import * as Y from 'yjs'; import { HocuspocusProvider } from '@hocuspocus/provider'; function CollaborativeEditor({ documentId }) { const doc = useMemo(() => new Y.Doc(), []); const provider = useMemo(() => new HocuspocusProvider({ url: process.env.NEXT_PUBLIC_HOCUSPOCUS_URL, name: `doc:${documentId}`, document: doc, token: getAuthToken(), onStatus: ({ status }) => console.log('Provider status:', status) }), [documentId, doc]); const editor = useEditor({ extensions: [ StarterKit.configure({ history: false }), Collaboration.configure({ document: doc }), CollaborationCursor.configure({ provider, user: { name: currentUser.name, color: generateUserColor(currentUser.id) } }) ] }); return ( <div className="editor-container"> <CollaboratorsAvatars provider={provider} /> <EditorContent editor={editor} /> </div> ); } function CollaboratorsAvatars({ provider }) { const [users, setUsers] = useState([]); useEffect(() => { const awareness = provider.awareness; const updateUsers = () => { const states = Array.from(awareness.getStates().values()); setUsers(states.filter(s => s.user).map(s => s.user)); }; awareness.on('change', updateUsers); updateUsers(); return () => awareness.off('change', updateUsers); }, [provider]); return ( <div className="collaborators"> {users.map(user => ( <Avatar key={user.id} name={user.name} color={user.color} title={`${user.name} сейчас редактирует`} /> ))} </div> ); } Технические детали реализации на Yjs
Для снижения нагрузки на сеть используем инкрементальное обновление состояния через encodeStateAsUpdate. При отключении клиента буфер изменений не теряется — при переподключении они передаются пачкой. Сжатие данных — стандартная GZip-компрессия, уменьшающая трафик на 60%.
Liveblocks (managed)
import { createClient } from '@liveblocks/client'; import { createRoomContext } from '@liveblocks/react'; import * as Y from 'yjs'; import { LiveblocksYjsProvider } from '@liveblocks/yjs'; const client = createClient({ publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_KEY }); const { RoomProvider, useRoom } = createRoomContext(client); function EditorPage({ documentId }) { return ( <RoomProvider id={`document-${documentId}`} initialPresence={{}}> <CollaborativeEditorWithLiveblocks /> </RoomProvider> ); } function CollaborativeEditorWithLiveblocks() { const room = useRoom(); const doc = useMemo(() => new Y.Doc(), []); useEffect(() => { const provider = new LiveblocksYjsProvider(room, doc); return () => provider.destroy(); }, [room, doc]); const editor = useEditor({ extensions: [ StarterKit.configure({ history: false }), Collaboration.configure({ document: doc }) ] }); return <EditorContent editor={editor} />; } Процесс работы
- Аналитика — обсуждаем сценарии использования, нагрузку, требования к персистентности.
- Проектирование — выбираем стек (Yjs/Liveblocks), проектируем модель данных, прописываем права доступа.
- Реализация — настраиваем сервер, интегрируем редактор, реализуем курсоры, аватары, автосохранение.
- Тестирование — нагрузочные тесты с эмуляцией 100+ одновременных пользователей.
- Деплой — развёртываем на вашем хостинге (Docker, Vercel, Cloudflare). Передаём документацию и доступы.
- Поддержка — гарантия 3 месяца на выявленные баги, возможность продления.
Что входит в результат
- Функциональный редактор с совместным редактированием, курсорами и аватарами.
- Бэкенд с персистентностью в вашей базе данных.
- Документация по API и доработке интерфейса.
- Обучение команды работе с Yjs.
Ориентировочные сроки
| Вариант | Сроки |
|---|---|
| Liveblocks интеграция (без сервера) | от 1 недели |
| Yjs + Hocuspocus с нуля | от 3 недель |
Частые ошибки при внедрении
- Игнорирование синхронизации состояний при отключении клиента (используем
awarenessс таймаутами). - Неправильная загрузка старых документов (используем инкрементальные обновления через
encodeStateAsUpdate). - Ограничение на размер документа — Yjs handle до 100 МБ без проблем, но лучше сохранять в блоб.
Получите консультацию нашей команды — оценим ваш сценарий и предложим оптимальное решение. Свяжитесь с нами, чтобы заказать внедрение real-time совместного редактирования уже сегодня.







