архитектурное ревью

Что не так и как выжать больше

Критика архитектуры на фактах из кода (7 экспертных линз) + дорожная карта perf/стабильности/безопасности/стоимости + тикет-лист.

Вердикт

Инженерно сильно (единый WebGL-canvas, on-demand рендер, инстансинг, бинарный protobuf, серверные скрипты), но есть 4 системных риска — следствие решений «ради простоты разработки», дорогих на масштабе.

Стабильностьодин canvas/контекст без app-восстановления
Стоимостьиндикаторы 100% на сервере, per-instance, без дедупа
Надёжность данныхнет heartbeat → «тихо стоящая цена»
Безопасность100% телеметрия+PII; ключи плейнтекстом
5
критичных
22
высоких
14
средних
1
низких

Сделано правильно (не трогать)

Стабильность

SevПроблемаEvidence
КРИТSingle canvas + один WebGL-контекст без app-восстановления
Потеря контекста (TDR/смена GPU/>16 контекстов) гасит весь терминал; после restore холст пустой
Pixi-only handleContextLost, forceCanvas:!1, нет Canvas2D-fallback графика
КРИТНет app-level heartbeat → «тихо стоящая цена»
Half-open HTTP/2-стрим висит «живым» без данных до TCP-таймаута ОС (30–120с)
keepalive только опция fetch; app ping/pong = 0
ВЫСvite:preloadError → reload() без счётчика
Reload-петля на протухшем деплое (старые имена чанков)
function h(n){n.preventDefault();location.reload()}
ВЫССырой throw "LayoutManager was not loaded"
Белый экран если ленивый чанк не загрузился
mount-эффект if(!fe.lm)throw…
ВЫСWorkspace flush только на beforeunload
Потеря разметки/рисунков при краше/OOM/iOS
saveDataTreeThrottled.flush() на beforeunload, throttle 5с
ВЫСSharedWorker cross-tab blast radius
Исключение/зависший reconnect в воркере роняет данные во ВСЕХ вкладках
onconnect добавляет порт каждой вкладки к одному streamer
СРЕДTextureGC привязан к кадрам
В простое (0 draws) текстуры не выгружаются → рост VRAM на одном контексте
maxIdle=3600 кадров, checkCountMax=600

Стоимость

SevПроблемаEvidence
КРИТИндикаторы: server-side, per-instance, без дедупликации
Compute растёт как N×M×U стримов, не как число формул; одинаковый RSI считается дважды
indicator_source_code в каждом запросе, indicator_key_salt (cache-buster), пин tpi_worker_instance_id
ВЫСНет client-side компьюта даже для SMA/EMA
Тривиальная математика = постоянная серверная нагрузка + стрим
нет pyodide/JS-индикаторов/.wasm в корпусе
ВЫССвечи синтезируются на лету из тиков
CPU на каждый запрос истории/экстраполяции, особенно мелкие ТФ
CandleOffsetApi/ExtrapolationApi (Unary)
ВЫСBf-reconnect индикаторов = ∞ без circuit-breaker
Thundering-herd добивает упавший indicator-сервис
maxAttempts:∞, 1s, exp, cap 60s
СРЕДГолый график тащит индикаторный рантайм
Свеча в SeriesItem.candle → даже price = индикатор identity
есть отдельный CandleStream, но chart core на нём не сидит
СРЕДSettings читаются по типам, несколько round-trip на старте
Лишний QPS на settings-сервис на синхронном пути init
api/settings?settingType=rendering/media/internal

Безопасность

SevПроблемаEvidence
КРИТSentry 100% + PII + Replay без маскирования
GDPR ст.44 (US-инстанс) + видео балансов/позиций/ключей
tracesSampleRate:1, sendDefaultPii:!0 ×7 воркеров; replay без maskAllText
КРИТБиржевые/AI-ключи плейнтекстом на сервере
Компрометация бэкенда = доступ к торговым аккаунтам всех
StoreApiKey.credentials (T:9), POST api/ai-chat/api-keys {apiKey}
ВЫСWebhook = SSRF-поверхность
Бэкенд дёргает произвольный URL юзера без видимого allowlist
TestUserWebhook{webhook.url}
ВЫСПрава ключа не enforced (есть withdraw)
Клиент не настаивает на read-only/без-вывода
PermissionInfo{name,enabled} — только репорт
ВЫСaccessToken в JS-памяти + Bearer
XSS эксфильтрует токен (httpOnly не спасает)
setCredentials, Bearer ${accessToken}
ВЫСPostHog autocapture + recording + always
Утечка UX-контента финансового продукта в аналитику
person_profiles:"always", autocapture:!0
СРЕДViewport maximum-scale=1
Блокировка зума = нарушение WCAG 1.4.4
meta maximum-scale=1,minimum-scale=1
СРЕДДоки советуют «Allow unsupervised» для MCP
Приучает отключать human-in-the-loop у торгового аккаунта
MCP guide Step 4

Рендер

SevПроблемаEvidence
ВЫСГлобальный ticker.update() перерисовывает все сцены
Тик одного из 6 графиков = рендер всех на общем контексте
общий scenes[], нет per-scene scissor
ВЫСРендер на main-thread, OffscreenCanvas не для GL
GL+picking+Canvas2D-стакан конкурируют со Svelte/protobuf
transferControlToOffscreen = 0
ВЫСПолный реаллок Float32Array вместо bufferSubData
GC-давление + полный re-upload при скролле/смене ТФ
_vertexBuffer.data=new Float32Array(...)
СРЕДrAF busy-poll крутится вечно при 0 draws
Лишние wakeup main-thread в простое/фоне
петля requestAnimationFrame(n) безусловная
СРЕДЦвета как 4×float32 вместо packed uint32
4× overhead VRAM/полосы на инстанс
aStrokeColor 4 FLOAT, i+=16
СРЕДGPU-picking = второй render-pass, не throttled
Лишний pass на каждый pointermove
isRenderInteractive, нет rAF-коалесинга
СРЕДMarket Depth на Canvas2D (CPU), не GPU
5 слоёв стакана растеризуются на CPU параллельно GL
getContext("2d") в TxOrderBook
НИЗPixi v7 (WebGL2), не v8/WebGPU · powerPreference default · antialias off
Нет WebGPU-throughput; чарт может уйти на iGPU; ступенчатые линии
версии 7.x, powerPreference:"default"

Загрузка

SevПроблемаEvidence
ВЫСglobal.css 824 КБ render-blocking на каждой странице
Нескипаемый ресурс даже на логине; Tailwind не пурджится
4427 правил, 2735 --tw-
ВЫСhighlight.js 948 КБ / 108 языков — статически
Любой роут с чатом/AI грузит все грамматики (нужно ~5)
цепочка CzHe8QoV→ByCio7PO→Eu1IcvlF (static)
ВЫСprotobuf+Connect дублируется в 6 воркерах
2.8 МБ, 6 копий рантайма (SharedWorker = свой bundle-root)
makeMessageType 38–130 на воркер
ВЫСPixi-монолит 3.1 МБ грузится целиком
Все плагины (filters/text/a11y) даже если нужны свечи
BgH94ARf.js, 241× pixi
СРЕД352 чанка / 12 МБ — чрезмерная фрагментация
Сотни round-trip + большой modulepreload-манифест
ls *.js = 352

Архитектура

SevПроблемаEvidence
ВЫСМонолит 3.1 МБ, 91 зависимый чанк
Правка тултипа инвалидирует кэш всего движка у всех
grep -l BgH94ARf = 91
ВЫСХард-форки LWC + Lumino без upstream-связи
Каждый секьюрити-патч апстрима мёрджить вручную навсегда
рендер переписан на Pixi; lm-* классы
ВЫС26 schema-версий миграций (DRAWING_MANAGER:11)
Накапливающийся хвост migration-функций, нельзя удалять
манифест версий в монолите
СРЕДBespoke-движки (TxOrderBook, block-editor, time-series)
Свои кодовые базы без community-поддержки
build, not buy

Данные

SevПроблемаEvidence
ВЫС6 воркеров без координации → refresh/reconnect-шторм
До 6 параллельных refresh; синхронная пачка реконнектов × вкладки
каждый воркер свой api/auth/token + qv()
СРЕДOHLCV в тяжёлой Decimal{i64,i64}-обёртке
~12 вложенных сообщений на бар (vs double в indicator.Bar)
candle.v1.Candle все поля Decimal
СРЕДПолная пересортировка стакана на каждый snapshot
O(n log n) ресорт обеих сторон после каждого реконнекта
Qg(e.bids,"desc")=[...t].sort()
СРЕДHTTP/2 head-of-line на лоссовых сетях
Потеря пакета стопорит ВСЕ стримы соединения (свечи+котировки+индикаторы)
один origin/воркер, нет QUIC/WS-изоляции

⚡ Quick wins

Low-effort, высокий импакт — делается за день-два.

ДействиеЭффектУсилиеОбласть
Sentry sampleRate 1→0.1, sendDefaultPii:false, replay maskAllText/Inputs/blockMedia−90% PII/стоимости + нет записи балансов/ключейlowБезоп/Стоим
highlight.js → core + 5 языков, остальное import()−880 КБ на роутах с чатом/AImedЗагрузка
Tailwind strict purge + critical-CSS split824 КБ → ~80 КБ render-blockingmedЗагрузка
Pinned vendor-chunk для Pixi (manualChunks)рушит cache-bust blast radius для 91 чанкаlowАрхит
Снять maximum-scale=1 (touch-action в canvas)фикс WCAG 1.4.4lowБезоп
Cap reload-петли (sessionStorage≤2) + убрать throw LayoutManagerнет белых экранов/петельlowСтабильн
Агрессивнее throttling_delay + filter_response (watchlist=только LAST)меньше quote-push без новых APIlowСтоим

🔧 Средние ставки

Заметный эффект, недели работы.

ДействиеЭффектУсилиеОбласть
Heartbeat-watchdog per-stream поверх qv()детект мёртвого соединения: десятки сек → единицыlowСтабильн/Данные
Единый auth-broker + общий jitter-bucket для 6 воркеровrefresh 6→1, сглаженный реконнект-фронтmedДанные
App-level webglcontextrestored-хук (re-resolve + isNeedRedraw)устраняет single-canvas крашmedСтабильн
Стандартные индикаторы (SMA/EMA/RSI/MACD/BB/VWAP) на клиентснимает основную массу индикаторных стримовmedСтоим
Расцепить свечи и indicator-рантайм (рисовать из CandleStream)голый график не зависит от R_PYTHON/WASMmedСтоим/Архит
Dirty-driven render + partial bufferSubData0 wakeup в простое; дешёвая подгрузка историиmedРендер
Webhook SSRF-защита (allowlist/блок private-IP/DNS-rebind)закрывает SSRFmedБезоп
Edge-кэш истории свечей (Unary → immutable бары)нет повторного синтеза при перемоткеmedСтоим
Circuit-breaker для ∞-reconnect профилейнет retry-шторма при outagemedСтабильн/Данные
visibilitychange/pagehide flush + IndexedDB-бэкап workspaceокно потери данных → секунды (покрыт iOS/OOM)medСтабильн

🏗 Большие ставки

Архитектурные изменения, но именно они снимают системные риски.

ДействиеЭффектУсилиеОбласть
Контентно-адресуемый кэш индикаторов + warm-pool с evictionкратное снижение compute (доминирующий драйвер)highСтоим
Расщепить 3.1 МБ монолит (pixi/engine/migrations-lazy)blast radius 91→<10; быстрее кэшhighАрхит
OffscreenCanvas + рендер в воркереснять GL/picking с main-thread (нет джанка)highРендер
Pixi v8/WebGPU + per-scene scissor; HTTP/3 (QUIC)throughput мультичарта; нет TCP head-of-linehighРендер/Данные
e2e/KMS-шифрование ключей + enforced read-only по умолчаниюкомпрометация бэкенда ≠ доступ к деньгамhighБезоп

Ранжирование по экономии денег

Что реально снизит серверные расходы — по убыванию. Доминирующий драйвер — серверный per-instance компьют индикаторов.

#РычагЭкономияПочему
1Контентно-адресуемый кэш индикаторов + warm-pool★★★★★Доминирующий драйвер. Дедуп популярных формул (RSI/EMA на BTC/ETH) = ×N→×1; eviction простаивающих процессов. Самая большая экономия compute/памяти.
2Стандартные индикаторы на клиент★★★★Большинство юзеров — стандартный набор. Убирает основную массу серверных индикаторных стримов/процессов.
3Candles-only путь (обход indicator-рантайма)★★★★Голый график — самый частый кейс — перестаёт жечь indicator-worker.
4Edge-кэш истории свечей/экстраполяции★★★Закрытые бары immutable → CDN снимает повторный синтез при перемотке, особенно мелкие ТФ.
5Sentry sampleRate 1→0.05 + drop PII★★★−10–20× Sentry-ingest и трафика через собственный /api/events. Нулевой продуктовый риск.
6PostHog: autocapture off + sample recording 3%★★Кратно меньше событийного объёма и нагрузки на self-proxy a.takeprofit.com.
7Consolidate воркеры / shared protobuf-рантайм★★Меньше дублей кода и потоков-конкурентов; меньше CPU на сессию.
8throttling_delay/filter_response для фоновых виджетов★★Меньше quote-push на неактивные подписки — всё уже в протоколе.
9Circuit-breaker (анти-thundering-herd)Не постоянная экономия, но срезает пиковую стоимость инцидента (retry-шторм ×вкладки).

Тикет-лист (готов в трекер)

28 issues, отсортированы по severity. Готовы к заведению.

IDЗаголовокОбластьSevУсилие
TP-1Sentry: sampleRate→0.1, sendDefaultPii:false, mask ReplayБезопКРИТlow
TP-2WebGL contextrestored: re-resolve снимка + isNeedRedrawСтабильнКРИТmed
TP-3App-level heartbeat-watchdog per-stream + stale-индикацияСтабильнКРИТlow
TP-4Шифрование биржевых/AI ключей + enforced read-only defaultБезопКРИТhigh
TP-5Content-addressable indicator cache + warm-pool evictionСтоимКРИТhigh
TP-6highlight.js → core + lazy языкиЗагрузкаВЫСmed
TP-7Tailwind purge + critical-CSS splitЗагрузкаВЫСmed
TP-8Pixi pinned vendor-chunk + расщепить монолитАрхитВЫСhigh
TP-9Единый auth-broker + общий jitter-bucket (6 воркеров)ДанныеВЫСmed
TP-10Стандартные индикаторы на клиент (JS/WASM)СтоимВЫСmed
TP-11Candles-only путь через CandleStreamСтоимВЫСmed
TP-12Webhook SSRF allowlist/private-IP blockБезопВЫСmed
TP-13Circuit-breaker для ∞-reconnect (Bf/fN)СтабильнВЫСmed
TP-14SharedWorker health + per-tab supervisorСтабильнВЫСhigh
TP-15reload-петля cap + убрать throw LayoutManagerСтабильнВЫСlow
TP-16visibilitychange/pagehide flush + IndexedDB-бэкапСтабильнВЫСmed
TP-17accessToken: убрать из логов/replay, CSP без unsafe-evalБезопВЫСmed
TP-18PostHog: identified_only + autocapture allowlist + sample recordingБезопВЫСlow
TP-19OHLCV double вместо Decimal для отрисовкиДанныеСРЕДmed
TP-20Инкрементальный orderbook-снапшот (diff по version)ДанныеСРЕДlow
TP-21Dirty-driven render (убрать вечный rAF) + bufferSubDataРендерСРЕДmed
TP-22Per-scene scissor вместо глобального ticker.updateРендерВЫСhigh
TP-23OffscreenCanvas + рендер в воркереРендерВЫСhigh
TP-24Throttle hit-test по rAFРендерСРЕДlow
TP-25Batched settings request + immutable cache headersЗагрузка/СтоимСРЕДlow
TP-26UPSTREAM.md baseline + cherry-pick процесс (LWC/Lumino)АрхитВЫСlow
TP-27Консолидировать миграции + golden-fixture тестыАрхитВЫСmed
TP-28Edge-cache истории свечей (CDN, immutable бары)СтоимВЫСmed

Метод

Ревью по реверс-инжинирингу клиента (352 чанка + 6 воркеров + 46 CSS + 99 доков). Каждая проблема подтверждена конкретным кодом/конфигом. Severity и усилие — экспертная оценка. Это анализ клиентской стороны; серверные числа (реальная стоимость compute) — оценочные, точные нужны от команды.