Corp/docs/reports/CHAT_REFACTORING_ANALYSIS.md

9.5 KiB
Raw Blame History

Отчёт: Рефакторинг чатов EUSRR + Сравнительный анализ

1. Что было сделано

1.1 Frontend — Декомпозиция монолита page.tsx

Метрика До После
Строк в page.tsx 1 989 893 (−55%)
useState в page.tsx 40+ 18 (UI-only)
useEffect в page.tsx 15+ 10
useCallback в page.tsx 20+ ~12
Выделенные хуки 0 3 новых

Новые файлы:

  • hooks/useMarkRead.ts — дебаунсированная отметка прочтения с очередью, watermark-синхронизация.
  • hooks/useChatMessages.ts — состояние сообщений, пагинация, pending/optimistic, unread-трекинг, навигация по дате, возврат к непрочитанным.
  • hooks/useChatScroll.ts — управление скроллом, floating date, кнопка «вниз», определение позиции.

1.2 Исправление бага со счётчиком непрочитанных

Причина: flushMarkRead после markChatAsRead вызывал getChat(chatId) и перезаписывал newMessagesBelowCount серверным значением (0), из-за чего бейдж исчезал мгновенно.

Решение: onReadAcknowledged в useMarkRead теперь не трогает локальный счётчик. Сброс newMessagesBelowCount происходит только когда пользователь физически доскроллит до низа (syncScrollToBottomState). Серверный chat.unread_count обновляется отдельно через setChat(fresh).

1.3 Backend — N+1 оптимизации

Проблема Файл Решение
get_last_message делал N запросов (1 на чат) serializers.py Добавлен Prefetch('messages', ...) с to_attr='_prefetched_last_message' в ChatViewSet.get_queryset
get_participant_names перезапрашивал memberships serializers.py Теперь использует уже prefetch'нутый obj.memberships.all()
Дублирующийся Q(participants=user) viewsets.py Удалён дубль

Уже было оптимизировано (не трогали):

  • reactions__user — prefetch в MessageViewSet.get_queryset ✓
  • ChatReadState.unread_count — денормализовано через сигнал ✓
  • react/unreact — уже используют select_related('user') ✓

2. Оценка нагрузки на БД

2.1 Список чатов (GET /chats/)

Ресурс Запросов (до) Запросов (после)
Основной запрос чатов 1 1
Prefetch memberships 1 1
Prefetch user_settings 1 1
Prefetch read_states 1 1
last_message N (1 на чат) 1 (prefetch)
participant_names N (1 на чат) 0 (из prefetch)
ИТОГО для 50 чатов ~104 ~5

2.2 Страница чата (GET /chats/{id}/, GET /messages/)

Операция Запросов
Загрузка чата 5 (chat + 4 prefetch)
Загрузка 30 сообщений вокруг anchor 1 + 4 prefetch = 5
WebSocket new_message 0 (из памяти)
mark-read (дебаунс 300ms) 1 POST + 1 GET (refresh chat)
Реакция 1 POST + 1 SELECT (summary)
Типичное открытие чата ~12 запросов

2.3 Узкие места (остаются)

  1. get_last_message в retrieve — для единичного getChat() prefetch не помогает (1 запрос). Допустимо.
  2. PollViewSet.vote — option.votes.count() в цикле (N+1). Нужен annotate(Count('votes')).
  3. ChatViewSet.create — User.objects.get(id=user_id) в цикле при добавлении участников. Нужен User.objects.filter(id__in=...).

3. Сравнение с популярными мессенджерами

3.1 Архитектура доставки сообщений

Фича Telegram WhatsApp Slack EUSRR
Транспорт MTProto (custom) Signal Protocol WebSocket WebSocket
Оффлайн-очередь Push + pull при connect Push + pull Pull при connect Pull при connect (fallback sync)
E2E шифрование Secret Chats only Да Enterprise only Нет (корп. сеть)
Оптимистичные сообщения Да Да Да Да ✓
Pending → Delayed → Failed Да (часы/галочки) Да Нет Да ✓

3.2 Управление прочтением

Фича Telegram WhatsApp Slack EUSRR
Подсчёт непрочитанных Денормализован Денормализован Денормализован (badge API) Денормализован (ChatReadState.unread_count) ✓
Mark-as-read При открытии (2с задержка) При открытии При фокусе канала При скролле до сообщения (300ms debounce)
Двойные галочки (read receipt) Да Да Нет Да (is_read, read_by) ✓
Кто прочитал Нет (кроме групп Premium) Нет Hover-подсказка Модальное окно ✓

3.3 Пагинация и скролл

Фича Telegram WhatsApp Slack EUSRR
Пагинация Бикурсорная (around + offset) Local-first DB (SQLite) Cursor-based Бикурсорная (before_id/after_id/around_id) ✓
Навигация по дате Да (календарь) Да (jump to date) Нет Да ✓
Scroll restoration position-to-bottom Anchor-based Виртуализация Anchor-based (getSnapshotBeforeUpdate) ✓
Виртуализация списка Нет (native) Нет (native) Да Нет ⚠️

3.4 Реакции

Фича Telegram WhatsApp Slack EUSRR
Быстрые реакции 5 недавних + picker 6 недавних + picker Несколько + picker 5 недавних + 40 emoji ✓
Custom emoji Premium Нет Workspace emoji Нет
Реакция-нотификация Push Push Push WebSocket ✓

3.5 Что есть у конкурентов, но нет у EUSRR

Фича Приоритет Сложность
Виртуализация списка сообщений (react-window) Высокий (>1000 сообщений = тормоза) Средняя
Реал-тайм обновление списка чатов (unread badge без перезагрузки) Высокий Низкая (WS уже есть)
Threads (ветки обсуждений) Средний Высокая
Поиск по сообщениям Средний Средняя
Голосовые сообщения Низкий Средняя
Пересланные сообщения (UI) Низкий Низкая

3.6 Что у EUSRR лучше аналогов

  1. Система ролей в чатах (admin/moderator/member/guest) — нет у Telegram/WhatsApp в таком виде.
  2. Интеграция с оргструктурой — чаты отделов, привязка к LDAP.
  3. Контекстные чаты (комментарии к постам/документам) — нет у классических мессенджеров.
  4. Управление доступом — гости без права отправки, модерация контента.

4. Рекомендации по дальнейшим улучшениям

Высокий приоритет

  1. Виртуализация списка сообщений — при >500 сообщений DOM замедляется. Использовать @tanstack/react-virtual или react-window.
  2. WebSocket обновление списка чатов — wsManager уже существует; нужен подписчик на new_message для обновления last_message и unread_count в списке без перезагрузки.
  3. Annotate poll vote counts — PollViewSet.vote имеет N+1 на option.votes.count(). Заменить на annotate(Count('votes')).

Средний приоритет

  1. State management — рассмотреть Zustand или TanStack Query для кеширования чатов/сообщений между страницами.
  2. Bulk create memberships — ChatViewSet.create делает N запросов при добавлении участников.
  3. Поиск по сообщениям — PostgreSQL full-text search (SearchVector, SearchQuery).

Низкий приоритет

  1. Удалить дублирование WebSocket логики (useWebSocket vs wsManager).
  2. Typed API client вместо any в createMessagesApi.