Оркестрация loading-состояний
Skeleton vs spinner vs progressive: бюджет тайминга, выбор техники, переходы без скачков.
Спроектируй оркестрацию loading для: {{screen}}. Источники: {{data}}.
Тезис: loading — не "что-то крутится". Это бюджет тайминга + выбор техники под форму данных + переходы, чтобы не было скачков.
1. Бюджет тайминга (правило 100/1000/10000)
| Длительность | Восприятие | Техника |
|---|---|---|
| < 100ms | мгновенно | ничего не показывай — оверлей мерцает |
| 100-300ms | быстро | 200ms fade-in; не показывай тяжёлый skeleton |
| 300ms-1s | заметно | skeleton ИЛИ inline-spinner |
| 1-3s | "грузится" | skeleton обязательно; для длинных списков — virtualized |
| 3-10s | "долго" | skeleton + контекст ("Получаем 1240 записей…") |
| > 10s | "сломалось" | прогресс-бар обязателен; кнопка отмены; иначе пользователь уходит |
Не уверен в длительности — измерь p50/p95 на проде. Дизайнить под "обычно быстро" — обманывать пользователя на p95.
2. Decision tree: какую технику
Известен прогресс (загрузка файла, multi-step)?
└── Да → progress bar (детерминированный)
└── Нет ↓
Форма результата предсказуема (список карточек, таблица)?
└── Да → skeleton, повторяющий форму
└── Нет ↓
Это сабмит / действие (кнопка, форма)?
└── Да → inline-spinner В кнопке + disable
└── Нет ↓
Это первичная навигация (route change)?
└── Да → top progress bar (NProgress-стиль) + skeleton ниже
└── Нет ↓
Это refresh уже видимых данных?
└── Да → subtle indicator (точка/spinner в углу), НЕ перерисовывай контент
└── Нет → spinner с подписью контекста
3. Skeleton — правила формы
- Силуэт совпадает по форме с контентом: высота строк, число колонок, размеры аватаров
- Ширина текстовых "плашек" вариативна (60%, 80%, 45%) — на пустых местах глаз ловит фальшь
- Радиусы и spacing — те же токены, что и в реальном UI
- Анимация —
background-positionshimmer ИЛИopacitypulse, не оба - Длительность анимации 1.4-1.8s; быстрее — нервно
prefers-reduced-motion→ выключи shimmer, оставь statiс серый
.skeleton {
background: linear-gradient(90deg,
var(--bg-subtle) 0%,
var(--bg-elevated) 50%,
var(--bg-subtle) 100%);
background-size: 200% 100%;
animation: shimmer 1.6s linear infinite;
}
@keyframes shimmer { to { background-position: -200% 0; } }
@media (prefers-reduced-motion: reduce) {
.skeleton { animation: none; }
}
4. Spinner — правила
- Размер 16/20/24px (контекст: в кнопке, в инпуте, на overlay)
- Толщина обводки = 2px при 16-20, 2.5-3px при 24+
- Анимация
rotate(360deg) linear 0.7-1s infinite— медленнее провоцирует "висит", быстрее раздражает - В кнопке — слева от лейбла, лейбл меняется на "Сохраняем…", кнопка disabled
5. Progress bar — правила
- Только если знаешь прогресс. Не делай fake-progress.
- Если шагов несколько (upload-OCR-parse) — покажи стадию ("Распознаём…")
- 0% → 100% линейно — не fake "почти готово"
- Если шаг непредсказуем по длине — переключи на indeterminate bar (
animation), не оставляй 90% мёртво
6. Переходы без скачков
Главная боль loading — CLS (layout shift) когда контент пришёл.
- Skeleton строго совпадает по высоте и ширине с конечным контентом
- Используй
min-heightна контейнере, чтобы первый кадр данных не подпрыгивал - Fade-in данных 150ms, fade-out skeleton 150ms — кроссфейд
- Картинки —
aspect-ratioзарезервирован до загрузки
{isLoading ? (
<SkeletonGrid count={6} />
) : (
<Grid items={data} className="animate-fadein" />
)}
7. Оркестрация нескольких запросов
Один экран — несколько запросов разной скорости. Выбор:
- All-or-nothing. Жди все, потом покажи. Только если части бессмысленны по отдельности.
- Streamed. Каждая секция отдельный skeleton; готовые появляются. Дефолт для дашбордов.
- Above-the-fold first. Главный блок — приоритет, остальное чуть позже. Использует Suspense / lazy / приоритеты запросов.
Анти-паттерн: верх ушёл, низ грузится — на каждый низ свой spinner ОДНОВРЕМЕННО. Лучше один общий статус "Подгружаем 3 раздела".
8. Optimistic UI (когда применимо)
- Применимо: like, toggle, sort, drag-reorder, добавление в корзину
- Не применимо: платежи, удаление с подтверждением, что угодно с настоящими последствиями
- На ошибке — откат + toast с retry. Контент не пропадает между.
9. Скрытый чек-лист (часто забывают)
- Первый paint не пустой — есть skeleton ИЛИ закешированные данные ИЛИ статичный shell
- Если данные пришли за < 100ms — skeleton НЕ мелькнул (debounce появления на 100ms)
- Skeleton имеет
aria-busy="true"иrole="status" -
aria-live="polite"объявляет завершение для screen reader - Кнопка отмены для всего, что > 3s
- На retry — старые данные на экране, новые подгружаются в фоне (не "пусто на 2с")
- Reduced-motion уважается
- Tested на throttled 3G (DevTools)
Формат вывода
- Тайминг-бюджет для каждого блока экрана (мс p50/p95 → техника)
- Decision tree применённый к {{screen}}: что какой техникой
- JSX-скелет skeleton-компонента, совпадающего с реальным
- Стратегия оркестрации для {{data}} (all/streamed/above-the-fold)
- Чек-лист с галочками что покрыто
Анти-паттерны
- Spinner поверх skeleton (двойная нагрузка глаз)
- Fake-progress, который застывает на 95%
- Skeleton мелькает 60ms — раздражает, ничего не сообщает
- На рефреш — пустой экран и заново skeleton; пользователь думает, всё пропало
- "Loading..." без контекста на 5 секунд
- Skeleton не совпадает по размеру → layout прыгает на финале
- Анимация skeleton 4s — выглядит "сломано", не "грузится"
Аудит производительности (Core Web Vitals)
Глубокая проверка LCP, INP, CLS с привязкой к коду и приоритизированным планом исправлений.
Billing-страница
Что показывать: план, история, способ оплаты, инвойсы, отмена.
Мастер-аудит сайта: 6 измерений за один проход
Orchestrator-аудит по 6 направлениям: UX, accessibility, performance, SEO, brand consistency, security. Quick scan + deep dive + приоритизированный план + композитная оценка + roadmap.