Резюме
Модернизация профессионального торгового терминала для проп-трейдинговой компании. Переход с традиционной React DOM архитектуры на Off-Main-Thread паттерн с использованием Canvas API, Web Workers и Protocol Buffers.
Ключевые бизнес-результаты:
- Производительность: стабильные 60/144 FPS при обработке потока 5000+ дельт/сек
- Латентность: Data-to-Pixel Latency < 16 мс (время обработки внутри приложения)
- Трафик: сокращение payload на 70% (JSON → Protobuf) — критично для мобильных сетей
- Safety: исключение ошибок округления в PnL (Decimal.js) и гарантия целостности данных (Gap Detection)
Контекст: Мы не торгуем со скоростью HFT (это FPGA и оптоволокно) — мы показываем данные с такой скоростью. Data-to-Pixel Latency < 16 мс означает, что наш код обрабатывает данные быстрее одного кадра (16 мс @ 60 Hz). Сетевую задержку мы не контролируем.
Технологический стек: TypeScript, React, Web Workers (OMT), OffscreenCanvas (Immediate Mode), Protobuf, BigInt + Decimal.js.
1. Проблематика: Пределы React DOM
1.1 Исходное состояние системы
Терминал представлял собой классическое React SPA с использованием useState/useReducer для управления состоянием котировок. WebSocket-соединение обрабатывало JSON-поток в Main Thread.
1.2 Идентифицированные узкие места
Таблица 1. Матрица производительности Legacy-терминала
| Компонент | Проблема | Влияние на UX |
|---|---|---|
| React Virtual DOM | Каждый тик цены вызывает Reconciliation всего дерева | Пропуск кадров |
| JSON Parsing | Синхронная десериализация блокирует Event Loop | Задержка обработки кликов |
| Main Thread | Все операции в одном потоке | Input Lag до 250мс |
| IEEE 754 Math | 0.1 + 0.2 ≠ 0.3 в JavaScript | Значительные расхождения в PnL |
| Text Rendering | DOM Layout для каждой ячейки стакана | 90%+ CPU на рендеринг |
1.3 Количественная оценка проблемы
Поток данных: 5 000 обновлений/сек
React setState: ~1мс на вызов
Reconciliation: ~5мс на 1000 элементов
DOM Paint: ~10мс на кадр
Итого: 5000 × 1мс = 5 сек/сек на setState
→ Невозможно обработать в реальном времениВывод: Архитектура React DOM физически неспособна обрабатывать высокочастотные потоки данных. Требуется фундаментальный пересмотр рендеринг-пайплайна.
2. Архитектурные решения
2.1 Off-Main-Thread Architecture (OMT)
Мы применили паттерн из GameDev-индустрии: разделение на потоки обработки и рендеринга.
Рис. 1. Архитектура потоков данных. Main Thread занят только обработкой ввода и отображением готовых bitmap. Вся тяжёлая работа вынесена в Workers.
2.2 Обоснование технологического стека
2.2.1 Canvas API vs React DOM
Таблица 2. Сравнение подходов рендеринга
| Критерий | React DOM | Canvas API (Imperative) |
|---|---|---|
| Обновление 1000 ячеек | ~15мс | ~0.5мс |
| Memory Overhead | 1 DOM Node = ~1KB | 1 Pixel = 4 bytes |
| Layout Thrashing | Да | Нет |
| Accessibility | Встроенная | Требует ARIA overlay |
| Сложность разработки | Низкая | Высокая |
Выбор технологии рендеринга
Canvas API (Imperative)
Pixel-perfect control без DOM reconciliation. Обновление 1000 ячеек за ~0.5мс вместо ~15мс.
SVG
DOM-накладные расходы при высокой частоте обновлений.
2.2.2 Protocol Buffers vs JSON
Таблица 3. Сравнение форматов сериализации
| Параметр | JSON | Protocol Buffers | Улучшение |
|---|---|---|---|
| Размер пакета (Order Book) | 2.4 KB | 0.7 KB | 3.4× меньше |
| Время парсинга (1000 msg) | 45мс | 8мс | 5.6× быстрее |
| Типизация | Runtime проверки | Compile-time схема | Безопаснее |
| Backward Compatibility | Хрупкая | Встроенная | — |
Выбор формата сериализации
Protocol Buffers
Compile-time схема с генерацией типов. 3.4× меньше размер пакета, 5.6× быстрее парсинг.
MessagePack
Нет строгой схемы и генерации типов.
2.2.3 Web Workers vs Single Thread
Таблица 4. Сравнение моделей конкурентности
| Сценарий | Single Thread | Web Workers |
|---|---|---|
| Flash Crash (50k msg/sec) | UI Freeze 5+ сек | UI responsive |
| CPU Utilization | 100% Main Thread | Распределено по ядрам |
| Input Latency | 100-500мс | менее 16 мс |
| Debugging | Простой | Требует DevTools опыта |
3. Механики производительности
3.1 Сжатие потока сообщений
При 5000 msg/sec отрисовывать каждое обновление невозможно — монитор показывает только 60 кадров.
Рис. 2. Паттерн Conflation. 5000 входящих сообщений сливаются в 60 снапшотов состояния, синхронизированных с частотой монитора.
Принцип: Последнее значение для каждой цены "побеждает". Отправка снапшота синхронизирована с requestAnimationFrame — не чаще 60 раз/сек. Данные передаются через Transferable Objects для Zero-Copy transfer.
3.2 OffscreenCanvas & Zero-Copy Transfer
Механика: Canvas передаётся в Render Worker через transferControlToOffscreen(). Это Zero-Copy операция — Main Thread полностью освобождается для обработки пользовательского ввода. React остаётся только shell-оболочкой с placeholder для Canvas.
3.3 Безопасная финансовая математика (BigInt + Decimal.js)
Важное архитектурное решение: При 5000 msg/sec с 20+ полями = 100 000 операций/сек. Библиотеки произвольной точности (BigNumber) создают новые объекты в Heap на каждую операцию, что перегружает Garbage Collector.
Решение: Разделение ответственности:
- BigInt — для ядра агрегации. На порядок быстрее, не грузит память.
- Decimal.js — только на финальном этапе форматирования для UI.
Проблема IEEE 754: 0.1 + 0.2 !== 0.3 в JavaScript (результат: 0.30000000000000004). При больших объёмах это приводит к значительным расхождениям в расчёте прибыли.
Архитектурное решение:
- BigInt в ядре — цена хранится как
BigIntс фиксированным масштабом 10^8. Операции без аллокаций, минимальная нагрузка на GC. - Decimal.js только для UI — precision: 20, ROUND_HALF_UP. Вызывается 60 раз/сек, не 100k раз/сек.
3.4 Circuit Breaker (Защита от перегрузки)
При Flash Crash поток может скачнуть до 50 000 msg/sec. Без защиты — Out of Memory. Circuit Breaker срабатывает при превышении порога (2000 сообщений в очереди): сбрасывает буфер, запрашивает полный снапшот с сервера и восстанавливается через 1 секунду.
3.5 Gap Detection (Контроль целостности данных)
При 5000 msg/sec пакеты могут теряться. Если мы потеряем один пакет с дельтой, весь Order Book станет неверным — цена «разъедется».
Механика: Каждое сообщение содержит sequence number. При обнаружении разрыва:
- Малый gap (≤5 пакетов): запрос микро-снапшота только недостающих данных
- Критический gap (>5 пакетов): полная ресинхронизация с сервера
Гарантия для трейдера: Терминал мгновенно запрашивает недостающие данные, блокируя интерфейс на миллисекунды. Трейдер никогда не видит ложную цену.
3.6 Пул объектов (борьба со сборщиком мусора)
Для снижения нагрузки на GC внедрён паттерн Object Pooling в воркерах — переиспользование объектов сообщений вместо создания новых. Pre-allocation 1000 объектов при старте, acquire/release в hot path.
Результат: Устранение микро-фризов (Jank) от сборщика мусора. GC pauses снизились с 50-100мс до < 5мс.
4. Результаты и метрики
4.1 Сравнительный анализ производительности
Таблица 5. Ключевые метрики: Legacy vs OMT Architecture
| Метрика | React DOM (Legacy) | Canvas + Workers (New) | Улучшение |
|---|---|---|---|
| FPS | 10-15 | 60 | 4-6× |
| Input Latency | 100-250мс | менее 16 мс | 6-15× |
| CPU Usage | 95% | 30% | 3× |
| Memory | 150MB | 45MB | 3.3× |
| Network | 5 Mbps | 1.5 Mbps | 3.3× |
| Parse Time | 45мс/1000 msg | 8мс/1000 msg | 5.6× |
4.2 Data-to-Pixel Latency Breakdown
Таблица 6. Декомпозиция задержки внутри приложения
| Этап | Legacy | New Architecture | Экономия |
|---|---|---|---|
| JSON Parse | 15мс | — | 15мс |
| Protobuf Decode | — | 2мс | — |
| React Reconciliation | 30мс | — | 30мс |
| Canvas Draw | — | 3мс | — |
| DOM Paint | 20мс | — | 20мс |
| Bitmap Transfer | — | 1мс | — |
| Total (Data-to-Pixel) | 65мс | 6мс | 59мс |
Примечание: Network RTT исключен из таблицы, т.к. мы контролируем только обработку внутри приложения. Data-to-Pixel < 16 мс = быстрее одного кадра.
4.3 Бизнес-результаты
- Удовлетворённость трейдеров: жалобы на "тормоза" снизились до нуля
- Вовлечённость: значительный рост среднего времени в терминале
- Торговая активность: рост объёма сделок (корреляция с UX)
- Трафик: снижение затрат на CDN за счёт бинарного протокола
5. Заключение и рекомендации
Off-Main-Thread Architecture с использованием OffscreenCanvas, Web Workers и Protocol Buffers — рабочий подход к созданию профессионального терминала для визуализации высокочастотных торговых потоков на Web-платформе.
Ключевые выводы:
- OffscreenCanvas устраняет накладные расходы DOM и обеспечивает pixel-perfect контроль
- Web Workers изолируют тяжёлые вычисления от UI thread, гарантируя отзывчивость
- Protocol Buffers сокращают трафик и время парсинга в 3-5 раз
- BigInt + Decimal.js — разделение ответственности: скорость в ядре, точность в UI
- Gap Detection + Object Pooling — гарантия целостности данных и стабильности GC
- Conflation — обязательный паттерн для high-frequency data streams
Рекомендация: Данная архитектура применима к любым real-time приложениям с высокой частотой обновлений: мониторинг IoT, live dashboards, collaborative editors, online gaming.