Perceived Performance
Perceived Performance — насколько быстрым ощущается интерфейс вне зависимости от реального времени загрузки. Мгновенная обратная связь, skeleton-состояния и optimistic UI создают ощущение скорости даже при медленных запросах.
Принципы
MUST
- Порог визуального отклика — 100ms — любое действие пользователя подтверждается мгновенно.
- Индикация ожидания — для операций длиннее 100ms обязателен промежуточный статус.
- Прогрессивная загрузка — контент отрисовывается в порядке: Layout → LCP → второстепенные модули.
- Стабильность — контент не прыгает и не сдвигается в процессе загрузки (CLS < 0.1). Резервировать место под динамический контент через
aspect-ratio,min-height.
SHOULD
- Optimistic UI — для предсказуемых действий (лайки, избранное, переключатели) состояние обновляется до ответа сервера. Обязателен rollback при ошибке.
- Skeleton вместо пустого состояния — пользователь видит структуру страницы до загрузки данных.
FORBIDDEN
- Оставлять действие без визуального отклика — кнопка не должна выглядеть «мёртвой» после клика.
- Блокировать весь интерфейс при ожидании ответа от сервера.
React Concurrent Features
MUST
Использовать Concurrent Features для тяжёлых обновлений:
useTransition — для тяжёлых обновлений (переключение вкладок, фильтрация). Не блокирует пользовательский ввод.
'use client';
import { useState, useTransition } from 'react';
export function Search() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
return (
<>
<input
value={query}
onChange={(e) =>
// обновление query — не срочное, не блокирует ввод
startTransition(() => setQuery(e.target.value))
}
/>
{isPending && <Spinner />}
</>
);
}
useDeferredValue — для отложенного обновления UI при быстром вводе.
'use client';
import { useState, useDeferredValue } from 'react';
export function FilteredList({ items }: { items: string[] }) {
const [filter, setFilter] = useState('');
const deferredFilter = useDeferredValue(filter); // обновляется с задержкой
const filtered = items.filter((i) => i.includes(deferredFilter));
return (
<>
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
<List items={filtered} />
</>
);
}
Optimistic UI — useOptimistic
'use client';
import { useOptimistic } from 'react';
export function LikeButton({ initial }: { initial: number }) {
const [optimisticLikes, addOptimistic] = useOptimistic(initial);
async function onLike() {
addOptimistic((n) => n + 1); // мгновенно показываем +1
try {
await likeOnServer();
} catch {
// React автоматически откатит optimistic state при ошибке
}
}
return <button onClick={onLike}>Likes: {optimisticLikes}</button>;
}
Skeleton-состояния
Next.js — App Router (автоматически)
// app/products/loading.tsx
// Автоматически становится fallback для Suspense
export default function Loading() {
return <ProductsSkeleton />;
}
Next.js — Pages Router (вручную)
export default function ProductsPage() {
const { data, isLoading } = useProducts();
if (isLoading) return <ProductsSkeleton />;
return <ProductsList items={data} />;
}
Astro — постепенная инициализация
---
import Reviews from '../components/Reviews.jsx';
---
<!-- Инициализируется когда browser idle, не блокируя первый экран -->
<Reviews client:idle />
React Side Effects
FORBIDDEN
- Использовать
useEffectдля синхронизации state (когда одинuseStateзависит от другого).
// ❌ Плохо: useEffect для синхронизации state
const [items, setItems] = useState(data);
const [filtered, setFiltered] = useState(items);
useEffect(() => {
setFiltered(items.filter(...)); // лишний рендер
}, [items]);
// ✅ Хорошо: derived state в теле компонента
const [items, setItems] = useState(data);
const filtered = items.filter(...); // вычисляется при рендере
MUST
Обязательная очистка в useEffect:
useEffect(() => {
const timer = setTimeout(() => {}, 1000);
const interval = setInterval(() => {}, 500);
window.addEventListener('resize', handler);
const subscription = subscribe();
return () => {
clearTimeout(timer);
clearInterval(interval);
window.removeEventListener('resize', handler);
subscription.unsubscribe();
};
}, []);