Как мы оптимизировали real-time синхронизацию в Aninix и столкнулись с неожиданной деградацией производительности из-за JSON.parse

В Aninix одна из ключевых фич — это возможность работать над анимацией вместе с другими пользователями в реальном времени. Чтобы это работало эффективно, мы разработали свою модель данных, вдохновившись принципами event sourcing. Вместо хранения конечного состояния проекта мы сохраняем цепочку изменений (patches). Такой подход позволяет как откатываться к любому моменту в истории, так и удобно синхронизировать работу нескольких человек.

Эволюция системы

В первой версии бэкенд просто уведомлял клиентов об изменениях, после чего они самостоятельно запрашивали обновления через REST API. Решение работало стабильно, но производительность оставляла желать лучшего.

Следующим логичным шагом стала прямая передача изменений. Теперь, когда пользователь вносит правки, операция сразу отправляется на бэкенд, сохраняется в базе и рассылается всем подключенным клиентам.

Неожиданный bottleneck

Для надёжной доставки сообщений мы используем Redis PubSub. Однако Redis работает только с бинарными данными (Buffer) или строками. В качестве быстрого решения мы реализовали сериализацию через JSON.stringify, а на принимающей стороне — десериализацию через JSON.parse.

График нагрузки на CPU

На графике хорошо видно, как после внедрения JSON.parse производительность системы существенно просела. После нескольких дней мониторинга в периоды пиковых нагрузок мы приняли решение отправлять сериализованные строки напрямую клиентам, переложив десериализацию на их сторону.

Планы на будущее

В следующих итерациях мы планируем перейти на бинарный протокол передачи данных. Это должно дать дополнительный прирост производительности и более эффективное использование ресурсов.

Этот случай хорошо показывает, как даже простые и привычные операции вроде JSON.parse могут стать узким местом при определённых сценариях использования. Постоянный мониторинг метрик помог нам вовремя заметить проблему и найти подходящее решение.