Анимация ожидания: Как мы обманываем мозг, пока API генерирует ответ

21.06.2026 13:00

Наш мозг физически не переносит пустоту. Когда пользователь нажимает кнопку «Сгенерировать отчет» или «Написать код», у нас есть ровно 400 миллисекунд. Это Порог Доэрти — критическая черта, после которой человек теряет ощущение контроля над интерфейсом. Если экран остается статичным дольше, возникает тревога: зависло приложение, отвалился интернет или сервер упал с 500-й ошибкой?

Проблема в том, что современные LLM-модели не умеют отвечать за 400 миллисекунд. Генерация качественного ответа на 2000 токенов занимает секунды, а иногда и десятки секунд. Бэкенд упирается в вычислительные лимиты GPU. Мы оказались в ситуации, когда ускорить сам процесс генерации на стороне ИИ невозможно. Значит, нам нужно ускорять восприятие времени.

В этой статье я разберу инженерную эволюцию того, как мы прячем сетевые задержки за UX-иллюзиями. Мы пройдем путь от примитивных скелетонов до Server-Sent Events и искусственного сглаживания потока, а также посмотрим, как интеграция RouterAPI решает проблему нестабильных ответов.

Фаза 1: Спиннеры мертвы. Да здравствуют скелетоны

Исторически первым решением был спиннер — бесконечно крутящееся колесо загрузки. Сегодня спиннер считается антипаттерном в тяжелых интерфейсах. Он символизирует неизвестность. Спиннер говорит «жди», но не дает никакой информации о том, сколько еще ждать и что именно происходит. Пассивное ожидание всегда кажется дольше активного.

Мы заменили спиннеры на скелетные загрузки (skeleton screens). Скелетон выполняет важнейшую психологическую задачу: он дает ложное чувство прогресса. Пользователь видит структуру будущего ответа — серые прямоугольники заголовков, списков и абзацев. Мозг дорисовывает картину и успокаивается.

Чтобы скелетон работал, он не должен быть статичным. Мы используем эффект «shimmer» (мерцание), который плавно движется слева направо, имитируя направление чтения.

.skeleton-line {
 height: 16px;
 background: #e2e8f0;
 background-image: linear-gradient(
 90deg,
 rgba(255, 255, 255, 0) 0,
 rgba(255, 255, 255, 0.4) 20%,
 rgba(255, 255, 255, 0) 40%
 );
 background-size: 200px 100%;
 background-repeat: no-repeat;
 animation: shimmer 1.5s infinite linear;
 border-radius: 4px;
}

@keyframes shimmer {
 0% { background-position: -200px 0; }
 100% { background-position: calc(200px + 100%) 0; }
}

Но у скелетонов есть предел прочности. Магия рассеивается примерно через 3 секунды. Если за это время реальные данные не начали поступать, пользователь понимает, что его обманули. Интерфейс выглядит загруженным, но остается пустым. Нам нужно подавать реальный контент.

Фаза 2: SSE-стриминг и RouterAPI

Ждать полного завершения генерации от LLM — самоубийство для конверсии. Текст нужно показывать по мере его появления, токен за токеном.

Первой мыслью инженера часто становятся WebSockets. Но тащить двунаправленный протокол, настраивать пинги, понги и следить за состоянием соединения ради однонаправленной задачи — это архитектурный оверинжиниринг. Нам не нужно слушать клиента, нам нужно непрерывно пушить ему текстовые чанки.

Идеальный инструмент для этого — Server-Sent Events (SSE). Он работает поверх обычного HTTP, поддерживает автоматическое переподключение из коробки и легко балансируется через стандартный Nginx.

Здесь мы сталкиваемся с суровой реальностью бэкенда. Зоопарк ИИ-моделей (OpenAI, Anthropic, локальные LLM через vLLM) отдает потоки по-разному. У одних провайдеров отваливается коннект на середине слова, у других меняется структура JSON в чанках, третьи уходят в таймаут. Писать обработчики под каждого провайдера на клиенте — значит превратить фронтенд в хрупкий монолит.

В нашем стеке эту проблему полностью закрывает интеграция RouterAPI. RouterAPI выступает единым интеллектуальным шлюзом. Фронтенд отправляет один стандартный POST-запрос, а RouterAPI берет на себя всю грязную работу. Он маршрутизирует запрос к оптимальному провайдеру, нормализует стоимость, а главное — отдает нам чистый, стандартизированный SSE-поток.

Если под капотом отвалится условный OpenAI, RouterAPI прозрачно переключится на фоллбек-модель. Для клиентского приложения поток просто немного замедлится, но не прервется ошибкой 502. Мы получаем мгновенный отклик: первый токен прилетает на фронтенд за те самые заветные 400 миллисекунд, заменяя скелетон на реальный текст.

Фаза 3: Эффект печатной машинки (The Typing Effect)

Казалось бы, задача решена. Бери чанки из SSE и вставляй в DOM. Но сеть непредсказуема. Токены приходят не равномерно, а рваными пачками (bursts). Текст на экране начинает дергаться: то замирает на секунду, то выплевывает сразу три абзаца текста. Это разрушает UX. Исчезает иллюзия того, что ИИ "думает и общается с тобой".

Чтобы исправить это, мы внедряем паттерн "буферизованного вывода". Мы не рендерим токены сразу. Мы складываем их в очередь и отдаем в UI с фиксированной, контролируемой скоростью, имитируя плавную печать текста человеком.

Вот как выглядит реализация такого механизма на React:

import { useState, useEffect, useRef } from 'react';

export function useStreamingText(apiUrl, requestBody) {
 const [displayedText, setDisplayedText] = useState('');
 const [isGenerating, setIsGenerating] = useState(false);
 const buffer = useRef('');
 const isTyping = useRef(false);

 useEffect( => {
 if (!apiUrl) return;

 let abortController = new AbortController;

 const fetchStream = async => {
 setIsGenerating(true);
 try {
 const response = await fetch(apiUrl, {
 method: 'POST',
 headers: { 'Content-Type': 'application/json' },
 body: JSON.stringify(requestBody),
 signal: abortController.signal
 });

 const reader = response.body.getReader;
 const decoder = new TextDecoder('utf-8');

 while (true) {
 const { done, value } = await reader.read;
 if (done) break;

 const chunk = decoder.decode(value, { stream: true });
 // Парсим SSE формат (data: {..})
 const lines = chunk.split('\n').filter(line => line.trim !== '');
 for (const line of lines) {
 if (line.startsWith('data: ')) {
 const dataStr = line.replace('data: ', '');
 if (dataStr === '[DONE]') continue;
 
 try {
 const parsed = JSON.parse(dataStr);
 const token = parsed.choices[0].delta.content || '';
 buffer.current += token;
 
 // Запускаем процесс "печати", если он еще не идет
 if (!isTyping.current) {
 processBuffer;
 }
 } catch (e) {
 console.error('Ошибка парсинга чанка', e);
 }
 }
 }
 }
 } catch (error) {
 if (error.name !== 'AbortError') {
 console.error('Ошибка стриминга:', error);
 }
 } finally {
 setIsGenerating(false);
 }
 };

 const processBuffer = => {
 if (buffer.current.length === 0) {
 isTyping.current = false;
 return;
 }

 isTyping.current = true;
 // Откусываем первый символ из буфера
 const char = buffer.current.charAt(0);
 buffer.current = buffer.current.slice(1);

 setDisplayedText(prev => prev + char);

 // Динамическая задержка: если буфер переполнен, печатаем быстрее
 const delay = buffer.current.length > 50 ? 5 : 20;
 setTimeout(processBuffer, delay);
 };

 fetchStream;

 return => abortController.abort;
 }, [apiUrl, requestBody]);

 return { displayedText, isGenerating };
}

В этом коде кроется главная хитрость: динамическая задержка (delay). Если сеть тормозит и буфер почти пуст, мы печатаем символы медленно (20 мс на символ), растягивая удовольствие и маскируя сетевую задержку. Если же сеть прорвало и в буфере скопилось 50+ символов, мы ускоряем печать до 5 мс, чтобы текст не отставал от реального ответа сервера слишком сильно.

Этот простой алгоритм превращает дерганый сетевой поток в гипнотическое, плавное появление текста. Пользователь перестает обращать внимание на задержки, потому что его взгляд прикован к постоянно обновляющемуся контенту.

Инженерия восприятия

Качественный UX строится на инженерии восприятия времени. Мы не можем заставить нейросеть с миллиардами параметров генерировать ответы мгновенно. Законы физики и архитектура трансформеров диктуют свои правила.

Но комбинируя правильные инструменты, мы берем время под контроль. Скелетоны покупают нам первые три секунды спокойствия. Надежный SSE-стриминг через RouterAPI гарантирует, что поток данных не оборвется и придет в едином формате, независимо от того, какая модель работает под капотом. А клиентский буфер с эффектом печатной машинки сглаживает сетевые флуктуации, превращая ожидание в залипательный процесс наблюдения.

В итоге пользователь не ждет ответа системы. Он видит, как система работает для него в реальном времени. И именно эта иллюзия отличает хороший продукт от выдающегося.

Теги

Ещё по теме