О проекте
Arcana — AI-компаньон для чтения карт таро: пользователи общаются с ботом через чат-интерфейс и получают персональные расклады. Бэкенд с логикой AI и знаниями о таро разрабатывала команда клиента. Наша зона ответственности — Flutter-клиент: быстрый, тщательно проработанный чат-интерфейс для iOS и Android.
Сама природа продукта сразу обозначила техническую проблему. Сессии в чате становятся длинными. Пользователи возвращаются ежедневно, история накапливается и может насчитывать тысячи сообщений. При этом AI стримит ответы в реальном времени — выводит форматированный текст с жирными выделениями, курсивом, заголовками и маркированными списками. Выстроить чат-экран, который корректно справляется с обоими сценариями при 60 fps на бюджетном железе, — вот в чём состояла суть нашей работы.

Карта дня

Стриминг ответа в чате

Расклад карт
Стриминг в реальном времени через SSE
Ответы AI поступают потоком токенов, а не единым пакетом. Мы реализовали SSE-клиент на Flutter, который открывает постоянное HTTP-соединение с бэкендом и обрабатывает поток токенов по мере их поступления. Каждый входящий фрагмент добавляется к активному сообщению в чате, создавая привычный эффект «печатания» — пользователь видит, как интерпретация появляется слово за словом.
Работа с SSE во Flutter потребовала аккуратного управления жизненным циклом соединения: переподключение при обрывах сети без потери частично полученного контента, буферизация неполных UTF-8-последовательностей на границах чанков, а также обновление только виджета активного сообщения, а не всего списка, при каждом новом токене.
Рендеринг Markdown на лету
AI форматирует ответы в Markdown: жирный шрифт для названий карт, курсив для ключевых слов, заголовки для разделов расклада, списки для ключевых тезисов. Поэтому рендерер чата должен был парсить и отображать Markdown инкрементально — обновляясь в реальном времени по мере поступления токенов.
Мы разработали собственный инкрементальный рендерер Markdown, который обрабатывает растущую строку при каждом обновлении, не перепарсивая всё сообщение с начала. Неполные токены в конце текущего буфера (незакрытый ** или недописанный заголовок) удерживаются в состоянии ожидания и отображаются как обычный текст, пока разделитель не закроется. Это исключает мерцание и скачки макета, сохраняя корректное форматирование по мере завершения ответа.
Бенчмарки производительности на бюджетных устройствах
Длинные истории чатов — норма для приложения с ежедневным использованием. Мы проектировали систему под 5 000+ сообщений без деградации и подтвердили это структурированными бенчмарками на бюджетных Android-устройствах — телефонах, представляющих нижний ценовой сегмент, где требования к производительности Flutter самые жёсткие.
Наш набор бенчмарков прокручивал списки сообщений разного объёма (500, 2 000 и 5 000 сообщений) с одновременным активным SSE-стримингом, фиксируя время кадра на протяжении всего теста. В начальных сборках при быстром скролле с отметки 2 000+ сообщений наблюдались просадки кадров: проходы лейаута для пузырьков с Markdown-рендерингом становились дорогими.
Мы устранили это несколькими точечными оптимизациями:
Ленивое кэширование лейаутов: Отрендеренные Markdown-лейауты кэшируются по ID сообщения. Как только для пузырька рассчитан лейаут, его размеры и дерево виджетов переиспользуются при последующих прокрутках вместо повторного вычисления.
Виртуализация через Sliver: Список чата построен на SliverList с кастомным делегатом, который не строит виджеты за пределами экрана. Инстанцируются только сообщения в пределах или вблизи видимого вьюпорта — дерево виджетов остаётся неглубоким независимо от общего числа сообщений.
Изолированные ребилды стрима: Потоковое сообщение внизу списка управляется отдельно от истории. Обновления SSE-токенов инициируют точечный ребилд только виджета активного пузырька, не затрагивая виртуализированный список.
После этих оптимизаций время кадра стабильно удерживалось ниже 16 мс во всех сценариях бенчмарка — включая историю из 5 000 сообщений с активным стримингом — на целевых бюджетных устройствах.
Результаты
Итоговый клиент остаётся плавным и отзывчивым при любой глубине истории и на любом устройстве целевого диапазона. Потоковые ответы отрисовываются в реальном времени с корректным Markdown-форматированием, а постоянные пользователи с сотнями сессий не замечают замедлений. Запас по производительности позволяет продукту расти, не пересматривая архитектуру рендеринга.
Клиент Arcana был создан в рамках нашей разработки на Flutter, где производительность рендеринга — один из ключевых приоритетов, и остаётся самым сложным кейсом рендеринга чата в нашей практике разработки социальных и комьюнити-приложений.
