Как решить проблему с зависанием страницы?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение проблемы зависания (freezing) страницы
Зависание страницы - это когда пользователь видит, что страница не реагирует на клики, скролл или ввод текста. Это одна из самых раздражающих проблем в веб-разработке. Давайте разберёмся, почему это происходит и как это исправить.
1. Основная причина - Main Thread блокирован
Вся JavaScript логика в браузере работает в одном потоке (Main Thread). Если код долго выполняется, страница зависает:
// ПЛОХО - зависает на 5 секунд
function processLargeData() {
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += Math.sqrt(i); // Тяжёлые вычисления
}
return result;
}
// Пользователь кликает на кнопку -> ничего не происходит
// Скролит страницу -> не скролится
// Это потому что main thread занят вычислениями
Иллюстрация работы main thread:
Время: 0s 1s 2s 3s 4s 5s
|-----------|-----------|-----------|-----------|-----------|
Main Thread: [████████████████ processLargeData() ██████████████████]
(невозможно обрабатывать другие события)
2. Диагностика проблемы с Chrome DevTools Performance
Нужно найти, что зависает:
// Шаг 1: Открой Chrome DevTools -> Performance tab
// Шаг 2: Нажми запись, выполни действие, которое зависает
// Шаг 3: Посмотри на диаграмму:
const analysis = {
'Длинная жёлтая полоса': 'Скрипт работает долго',
'Красный треугольник внизу': 'Janky frame - потеряны кадры',
'Frame rate упал': 'С 60fps упал до 10fps'
};
// Нажми на жёлтую полоску и видишь, какой код работает
3. Решение 1 - Разбить работу на части (Chunking)
Вместо одного долгого выполнения, разбей на маленькие кусочки:
// ПЛОХО - зависает
function processItems(items) {
items.forEach(item => {
expensiveOperation(item); // Для каждого элемента
});
}
processItems(millionItems);
// ХОРОШО - разбиваем на батчи
function processItemsInChunks(items, batchSize = 100) {
let index = 0;
function processBatch() {
const end = Math.min(index + batchSize, items.length);
for (let i = index; i < end; i++) {
expensiveOperation(items[i]);
}
index = end;
if (index < items.length) {
// Дай браузеру время обновить UI
requestAnimationFrame(processBatch);
}
}
processBatch();
}
processItemsInChunks(millionItems, 100);
// Результат: страница остаётся отзывчивой
Визуальная разница:
БЕЗ chunking: С chunking:
[████████████████] 5 сек [██] [██] [██] 5 сек
(зависла на 5 сек) (отзывчива 95% времени)
4. Решение 2 - Web Workers для вычислений
Для тяжёлых вычислений используй отдельный поток:
// main.js
const worker = new Worker('heavy-computation.js');
// Отправь данные на обработку
worker.postMessage({ data: millionItems });
// Получи результат (когда worker закончит)
worker.onmessage = (event) => {
const result = event.data;
updateUI(result);
};
// main thread остаётся свободным!
// heavy-computation.js (worker)
self.onmessage = (event) => {
const items = event.data.data;
// Долгие вычисления в отдельном потоке
const result = items.map(item => expensiveOperation(item));
// Отправляем результат обратно
self.postMessage({ result });
};
Преимущество: вычисления не блокируют UI.
5. Решение 3 - setTimeout и debounce/throttle
Для событий, которые срабатывают много раз:
// ПЛОХО - срабатывает при каждом движении мыши (сотни раз в сек)
window.addEventListener('mousemove', (e) => {
expensiveCalculation(e.x, e.y); // Зависает!
});
// ХОРОШО - используй throttle
function throttle(func, delay) {
let lastRun = 0;
return function(...args) {
const now = Date.now();
if (now - lastRun >= delay) {
func(...args);
lastRun = now;
}
};
}
const throttledCalc = throttle((e) => {
expensiveCalculation(e.x, e.y);
}, 100); // Максимум 10 раз в сек
window.addEventListener('mousemove', throttledCalc);
// Или с debounce - дождись, пока пользователь остановится
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
}
const debouncedSearch = debounce((query) => {
expensiveSearch(query);
}, 300);
input.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
6. Решение 4 - Виртуализация для больших списков
Если отрисовываешь 1000+ элементов списка - зависает:
// ПЛОХО - 1000 элементов в DOM
function renderList(items) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
// ХОРОШО - используй react-window или react-virtualized
import { FixedSizeList } from 'react-window';
function VirtualizedList({ items }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={35}
>
{({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
)}
</FixedSizeList>
);
}
// Результат: в DOM только видимые элементы (~20-30)
// Остальные вставляются/удаляются при скролле
7. Решение 5 - Code Splitting и Lazy Loading
Если зависает при загрузке страницы:
// ПЛОХО - весь код загружается сразу (500KB)
import HeavyComponent from './HeavyComponent';
// ХОРОШО - загружается только когда нужен
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
);
}
Результат: страница загружается быстро, тяжёлые компоненты загружаются по требованию.
8. Решение 6 - Optimized Rendering
Иногда проблема в том, как React отрисовывает компоненты:
// ПЛОХО - каждый render пересчитывает всё
function Parent() {
const [filter, setFilter] = useState('');
return (
<>
<Input onChange={setFilter} />
<HugeList items={allItems} /> {/* Пересчитывается при каждом изменении фильтра */}
</>
);
}
// ХОРОШО - используй useMemo и React.memo
const HugeListMemo = React.memo(HugeList);
function Parent() {
const [filter, setFilter] = useState('');
const filteredItems = useMemo(
() => allItems.filter(item => item.name.includes(filter)),
[filter]
);
return (
<>
<Input onChange={setFilter} />
<HugeListMemo items={filteredItems} />
</>
);
}
// Или используй useCallback для стабильных функций
const handleClick = useCallback(() => {
// ...
}, [dependency]);
9. Решение 7 - Оптимизация анимаций
Анимации могут вызвать зависание:
/* ПЛОХО - анимирует width (вызывает reflow) */
.box {
animation: slideWidth 1s;
}
@keyframes slideWidth {
from { width: 0; }
to { width: 100px; }
}
/* ХОРОШО - анимирует transform (не вызывает reflow) */
.box {
animation: slideTransform 1s;
}
@keyframes slideTransform {
from { transform: translateX(0); }
to { transform: translateX(100px); }
}
Transform анимируется на GPU, не зависает.
10. Полная диагностика зависания
Когда страница зависает, следуй этому алгоритму:
const diagnosis = [
'1. Открой Chrome DevTools -> Performance',
'2. Запиши что-то (5-10 секунд)',
'3. Нажми жёлтый значок (Show what happened during each frame)',
'4. Ищи frames, где FPS < 60 (обычно < 30)',
'5. Кликни на frame и видишь что работало',
'6. Посмотри:
- Если жёлтое (JavaScript) - проблема в скрипте
- Если фиолетовое (Rendering) - проблема в Layout/Paint
- Если зелёное (Painting) - проблема в стилях',
'7. Возьми наибольший блок и кликни на него',
'8. В открывшемся окне видишь точный вызов функции',
'9. Фиксишь эту функцию'
];
// Пример результата:
// expensiveSearch() - 3.5 сек
// renderItems() - 1.2 сек
// updateUI() - 0.8 сек
// -> Фокусируешься на expensiveSearch()
11. Практический пример - зависание при поиске
// ДО (зависает)
function SearchComponent() {
const [query, setQuery] = useState('');
const handleChange = (e) => {
setQuery(e.target.value);
// Фильтруем 1 миллион элементов при каждом вводе
const filtered = millionItems.filter(item =>
item.name.toLowerCase().includes(e.target.value.toLowerCase())
);
setResults(filtered);
};
return (
<>
<input onChange={handleChange} />
<ul>
{results.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</>
);
}
// ПОСЛЕ (оптимизировано)
function SearchComponent() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
// Дебаунсим поиск - ждём 300ms после последнего ввода
const debouncedSearch = useCallback(
debounce((searchQuery) => {
// Это вычисляется не при каждом вводе, а только когда пользователь остановился
const filtered = millionItems.filter(item =>
item.name.toLowerCase().includes(searchQuery.toLowerCase())
);
setResults(filtered);
}, 300),
[]
);
const handleChange = (e) => {
setQuery(e.target.value);
debouncedSearch(e.target.value);
};
return (
<>
<input onChange={handleChange} value={query} />
<VirtualizedList items={results} /> {/* Только видимые элементы в DOM */}
</>
);
}
Итог
Зависание страницы - это когда main thread блокирован долгим кодом. Решение: разбивай работу на части (chunking), используй Web Workers, применяй throttle/debounce для событий, виртуализируй большие списки, оптимизируй анимации с помощью transform, используй code splitting. Главное правило: не давай JavaScript работать больше 50ms подряд - это даст браузеру время обновить UI и остаться на 60fps.