Перейти к контенту

Профессиональный терминал для визуализации биржевых потоков: задержка < 16 мс

Разработка профессионального терминала для визуализации высокочастотных биржевых потоков. Архитектура выноса вычислений в фоновые потоки, рендеринг биржевого стакана на OffscreenCanvas и математика произвольной точности.

60 FPS
Стабильная частота кадров при потоке 5000+ обновлений/сек
-70%
Снижение трафика за счет бинарного протокола
< 16 мс
Задержка «данные-пиксель» (время обработки внутри приложения)
Безопасная математика
Исключение ошибок округления в расчётах прибыли
Обнаружение пропусков
Целостность данных при потере пакетов

Резюме

Модернизация профессионального торгового терминала для проп-трейдинговой компании. Переход с традиционной 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 Math0.1 + 0.2 ≠ 0.3 в JavaScriptЗначительные расхождения в PnL
Text RenderingDOM 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-индустрии: разделение на потоки обработки и рендеринга.

WSDataRenderMain Actions
100%
Ctrl+Колесо или перетаскивание

Рис. 1. Архитектура потоков данных. Main Thread занят только обработкой ввода и отображением готовых bitmap. Вся тяжёлая работа вынесена в Workers.

2.2 Обоснование технологического стека

2.2.1 Canvas API vs React DOM

Таблица 2. Сравнение подходов рендеринга

КритерийReact DOMCanvas API (Imperative)
Обновление 1000 ячеек~15мс~0.5мс
Memory Overhead1 DOM Node = ~1KB1 Pixel = 4 bytes
Layout ThrashingДаНет
AccessibilityВстроеннаяТребует ARIA overlay
Сложность разработкиНизкаяВысокая

Выбор технологии рендеринга

Архитектурное решение
Canvas API (Imperative)

Pixel-perfect control без DOM reconciliation. Обновление 1000 ячеек за ~0.5мс вместо ~15мс.

30× быстрее обновление
Нет Layout Thrashing
GameDev-паттерны для HFT
Отклонённый вариант
SVG

DOM-накладные расходы при высокой частоте обновлений.

Те же проблемы что React DOM
Reflow на каждый тик
Не подходит для 60 FPS

2.2.2 Protocol Buffers vs JSON

Таблица 3. Сравнение форматов сериализации

ПараметрJSONProtocol BuffersУлучшение
Размер пакета (Order Book)2.4 KB0.7 KB3.4× меньше
Время парсинга (1000 msg)45мс8мс5.6× быстрее
ТипизацияRuntime проверкиCompile-time схемаБезопаснее
Backward CompatibilityХрупкаяВстроенная

Выбор формата сериализации

Архитектурное решение
Protocol Buffers

Compile-time схема с генерацией типов. 3.4× меньше размер пакета, 5.6× быстрее парсинг.

Compile-time типизация
Backward Compatibility встроена
Кодогенерация для TypeScript
Отклонённый вариант
MessagePack

Нет строгой схемы и генерации типов.

Runtime проверки типов
Ошибки типов = финансовые убытки
Нет compile-time safety

2.2.3 Web Workers vs Single Thread

Таблица 4. Сравнение моделей конкурентности

СценарийSingle ThreadWeb Workers
Flash Crash (50k msg/sec)UI Freeze 5+ секUI responsive
CPU Utilization100% Main ThreadРаспределено по ядрам
Input Latency100-500мсменее 16 мс
DebuggingПростойТребует DevTools опыта

3. Механики производительности

3.1 Сжатие потока сообщений

При 5000 msg/sec отрисовывать каждое обновление невозможно — монитор показывает только 60 кадров.

5K msg/sBufferRAF60 FPS
100%
Ctrl+Колесо или перетаскивание

Рис. 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)Улучшение
FPS10-15604-6×
Input Latency100-250мсменее 16 мс6-15×
CPU Usage95%30%
Memory150MB45MB3.3×
Network5 Mbps1.5 Mbps3.3×
Parse Time45мс/1000 msg8мс/1000 msg5.6×

4.2 Data-to-Pixel Latency Breakdown

Таблица 6. Декомпозиция задержки внутри приложения

ЭтапLegacyNew ArchitectureЭкономия
JSON Parse15мс15мс
Protobuf Decode2мс
React Reconciliation30мс30мс
Canvas Draw3мс
DOM Paint20мс20мс
Bitmap Transfer1мс
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-платформе.

Ключевые выводы:

  1. OffscreenCanvas устраняет накладные расходы DOM и обеспечивает pixel-perfect контроль
  2. Web Workers изолируют тяжёлые вычисления от UI thread, гарантируя отзывчивость
  3. Protocol Buffers сокращают трафик и время парсинга в 3-5 раз
  4. BigInt + Decimal.js — разделение ответственности: скорость в ядре, точность в UI
  5. Gap Detection + Object Pooling — гарантия целостности данных и стабильности GC
  6. Conflation — обязательный паттерн для high-frequency data streams

Рекомендация: Данная архитектура применима к любым real-time приложениям с высокой частотой обновлений: мониторинг IoT, live dashboards, collaborative editors, online gaming.

Профессиональный терминал для визуализации биржевых потоков: задержка < 16 мс | Софтэнк