Исправлял ли проблему слишком нагруженного рендеринга страницы
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Оптимизация нагруженного рендеринга: проблемы и решения
Вопрос о производительности рендеринга — классический для собеседования. Это показывает практический опыт и понимание работы браузера.
Как диагностировать проблему
Chrome DevTools Performance:
// 1. Открыть Chrome DevTools (F12)
// 2. Вкладка Performance
// 3. Нажать Record
// 4. Взаимодействовать с страницей
// 5. Остановить Record
// Смотреть на:
// - Frames per second (FPS) — должно быть 60 FPS
// - Long tasks (жёлтые/красные блоки) — более 50ms
// - Layout (reflow) и Paint (repaint)
React DevTools Profiler:
// Вкладка Profiler в React DevTools
// Показывает:
// - Какие компоненты рендерятся
// - Сколько времени занимает рендеринг
// - Почему компонент перерендерился
Lighthouse:
// Chrome DevTools -> Lighthouse
// Оценивает Performance, Accessibility, Best Practices, SEO
// Даёт конкретные рекомендации
Типичные проблемы и решения
Проблема 1: Бесконечные ре-рендеры
Код с проблемой:
function DataList() {
const [items, setItems] = useState([]);
// ПРОБЛЕМА: useEffect вызывает setItems -> useEffect запускается снова
useEffect(() => {
setItems([1, 2, 3]); // Каждый раз новый массив
}, [items]); // items в зависимостях!
return <div>{items.map(item => <div key={item}>{item}</div>)}</div>;
}
Решение:
function DataList() {
const [items, setItems] = useState([]);
useEffect(() => {
setItems([1, 2, 3]);
}, []); // Пустые зависимости — запускается один раз
return <div>{items.map(item => <div key={item}>{item}</div>)}</div>;
}
Проблема 2: Ненужные ре-рендеры детей
Код с проблемой:
function Parent() {
const [name, setName] = useState('John');
const [count, setCount] = useState(0);
return (
<>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<p>Name: {name}</p>
{/* ПРОБЛЕМА: ExpensiveList перерендерится каждый раз,
когда меняется name, хотя она не зависит от name */}
<ExpensiveList count={count} />
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
</>
);
}
function ExpensiveList({ count }: { count: number }) {
console.log('ExpensiveList rendered'); // Логируется каждый раз!
// Дорогие вычисления
const items = Array.from({ length: 10000 }, (_, i) => i);
return (
<div>
Count: {count}
{items.map(item => (
<div key={item}>{item}</div>
))}
</div>
);
}
Решение: React.memo
const ExpensiveList = React.memo(function ExpensiveList({ count }) {
console.log('ExpensiveList rendered'); // Логируется только при изменении count
const items = Array.from({ length: 10000 }, (_, i) => i);
return (
<div>
Count: {count}
{items.map(item => (
<div key={item}>{item}</div>
))}
</div>
);
});
// Теперь ExpensiveList перерендеривается только если count меняется
Проблема 3: Большие списки без виртуализации
Код с проблемой:
function UsersList({ users }: { users: User[] }) {
return (
<div>
{/* ПРОБЛЕМА: 10000 элементов — DOM станет огромным */}
{users.map(user => (
<div key={user.id} style={{ padding: '10px' }}>
<p>{user.name}</p>
<p>{user.email}</p>
</div>
))}
</div>
);
}
// 10000 пользователей = 10000 DIV элементов в DOM = МЕДЛЕННО
Решение: Виртуализация (react-window)
import { FixedSizeList as List } from 'react-window';
function UsersList({ users }: { users: User[] }) {
const Row = ({ index, style }: { index: number; style: any }) => (
<div style={style}>
<p>{users[index].name}</p>
<p>{users[index].email}</p>
</div>
);
return (
<List
height={600}
itemCount={users.length}
itemSize={100}
width="100%"
>
{Row}
</List>
);
}
// Теперь в DOM только видимые элементы (например, 10-15)
// Остальные добавляются/удаляются по мере скролла
Проблема 4: Медленные операции в рендеринге
Код с проблемой:
function SearchResults({ query }: { query: string }) {
// ПРОБЛЕМА: Полнотекстовый поиск в рендеринге
const results = database.search(query);
// ПРОБЛЕМА: Тяжелые вычисления
const sortedResults = results
.map(result => ({
...result,
score: calculateComplexScore(result),
}))
.sort((a, b) => b.score - a.score);
return (
<div>
{sortedResults.map(result => (
<div key={result.id}>{result.title}</div>
))}
</div>
);
}
Решение: useMemo
function SearchResults({ query }: { query: string }) {
const results = useMemo(() => {
return database.search(query);
}, [query]);
const sortedResults = useMemo(() => {
return results
.map(result => ({
...result,
score: calculateComplexScore(result),
}))
.sort((a, b) => b.score - a.score);
}, [results]);
return (
<div>
{sortedResults.map(result => (
<div key={result.id}>{result.title}</div>
))}
</div>
);
}
Проблема 5: Слишком много событий (debounce/throttle)
Код с проблемой:
function SearchInput() {
const [query, setQuery] = useState('');
// ПРОБЛЕМА: При каждом символе запрос на сервер
// Пользователь печатает "prepbro" -> 7 запросов!
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
fetch(`/api/search?q=${e.target.value}`); // 7 запросов
};
return <input onChange={handleChange} />;
}
Решение: Debounce
import { useDebouncedCallback } from 'use-debounce';
function SearchInput() {
const [query, setQuery] = useState('');
// Запрос на сервер только после 500ms без печати
const debouncedSearch = useDebouncedCallback((value: string) => {
fetch(`/api/search?q=${value}`);
}, 500);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
debouncedSearch(e.target.value); // 1 запрос вместо 7
};
return <input onChange={handleChange} value={query} />;
}
Проблема 6: Блокирующий JavaScript
Код с проблемой:
function HeavyComponent() {
// Синхронный расчёт 1 миллиона чисел
const sum = Array.from({ length: 1_000_000 }, (_, i) => i).reduce((a, b) => a + b);
return <div>Sum: {sum}</div>; // Блокирует UI на несколько секунд
}
Решение: Web Worker или useTransition
function HeavyComponent() {
const [sum, setSum] = useState<number | null>(null);
const [isPending, startTransition] = useTransition();
useEffect(() => {
startTransition(() => {
// React поместит это в фоновый приоритет
const result = Array.from({ length: 1_000_000 }, (_, i) => i).reduce((a, b) => a + b);
setSum(result);
});
}, []);
return (
<div>
{isPending && <p>Calculating...</p>}
{sum !== null && <p>Sum: {sum}</p>}
</div>
);
}
Пример полного ответа на собеседовании
Да, я встречался с такой проблемой в своём предыдущем проекте. У нас была таблица со списком заказов из 5000 элементов. Когда пользователь прокручивал, FPS падала до 15-20.
Я сначала профилировал в Chrome DevTools и обнаружил два основных узких места:
- Каждый элемент таблицы был дорогим в рендеринге — содержал вложенные компоненты и сложные вычисления
- При прокрутке все 5000 элементов держались в DOM
Решение было двухуровневое:
- Обернул TableRow в React.memo, так как пропсы не менялись
- Внедрил виртуализацию с помощью react-window — в DOM остаются только видимые строки
После этого FPS вернулась к 55-60. Также заметил, что при поиске по таблице срабатывал дебаунс для фильтрации, так как без него было слишком много запросов на сервер.
Теперь я всегда начинаю с профилирования — не гадаю, где узкое место, а измеряю.
Инструменты для профилирования
- Chrome DevTools Performance — встроенный
- React DevTools Profiler — для React приложений
- Lighthouse — общая оценка производительности
- Web Vitals — ключевые метрики (LCP, FID, CLS)
- Bundle Analyzer — размер бандла
Заключение
Проблемы с рендерингом решаются через:
- Диагностику (Chrome DevTools, React Profiler)
- Идентификацию узких мест (что медленное?)
- Применение правильного решения:
- React.memo для дорогих компонентов
- useMemo для дорогих расчётов
- useCallback для стабильных функций
- Виртуализация для больших списков
- Debounce/throttle для частых событий
- Web Workers для тяжелых вычислений