Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как избежать блокировки Event Loop
Event Loop - это сердце JavaScript. Если его заблокировать тяжелыми вычислениями, приложение зависнет, интерфейс перестанет реагировать на клики. Я расскажу, как это предотвратить.
Как работает Event Loop
JavaScript однопоточный, и всё выполняется в одном потоке:
// 1. Синхронный код выполняется СРАЗУ
console.log('1. Start');
// 2. setTimeout отправляет задачу в очередь (не выполняет сразу)
setTimeout(() => {
console.log('2. Timeout');
}, 0);
// 3. Обещание (Promise) тоже в очередь
Promise.resolve().then(() => {
console.log('3. Promise');
});
// 4. Синхронный код продолжает выполняться
console.log('4. End');
// Порядок вывода: 1, 4, 3, 2
// Почему 3 перед 2? Потому что Promises (микротаски) выполняются раньше setTimeout (макротаски)
Проблема 1: Блокирующие вычисления
Если сделать тяжелые вычисления в главном потоке, интерфейс зависнет:
// ПЛОХО - блокирует UI на несколько секунд
function calculateBad() {
console.log('Calculating...');
let sum = 0;
for (let i = 0; i < 10_000_000_000; i++) {
sum += i;
}
console.log('Done'); // Интерфейс зависнет!
return sum;
}
button.addEventListener('click', calculateBad);
Решение 1: Разбить на части с setTimeout
Разделите работу на маленькие блоки и дайте браузеру время на обновление:
// ХОРОШО - работает асинхронно
async function calculateGood() {
let sum = 0;
const chunk = 100_000_000; // Обработать за раз
const total = 10_000_000_000;
for (let i = 0; i < total; i += chunk) {
// Обработать часть данных
for (let j = i; j < i + chunk && j < total; j++) {
sum += j;
}
// Дать Event Loop возможность обработать события
await new Promise(resolve => setTimeout(resolve, 0));
}
console.log('Done');
return sum;
}
button.addEventListener('click', calculateGood);
Решение 2: Web Workers для тяжелых вычислений
Для серьезных вычислений используйте отдельный поток:
// main.js
function calculateHeavy() {
// Создаем worker (отдельный поток)
const worker = new Worker('worker.js');
// Отправляем данные в worker
worker.postMessage({ number: 10_000_000_000 });
// Получаем результат когда готово
worker.onmessage = (event) => {
console.log('Result:', event.data);
// UI НИКОГДА не зависает!
};
worker.onerror = (error) => {
console.error('Worker error:', error);
};
}
// worker.js (выполняется в отдельном потоке)
self.onmessage = (event) => {
const { number } = event.data;
let sum = 0;
// Тяжелые вычисления БЕЗ блокирования главного потока
for (let i = 0; i < number; i++) {
sum += i;
}
// Отправляем результат обратно
self.postMessage(sum);
};
Проблема 2: Бесконечные циклы в обработчиках
Это классическая ошибка:
// ПЛОХО - зависает
input.addEventListener('input', (e) => {
// Бесконечный цикл - никогда не завершится
while (true) {
console.log(e.target.value);
}
});
// ХОРОШО - управляемая обработка
input.addEventListener('input', (e) => {
const value = e.target.value;
// Обработать значение без циклов
updateUI(value);
});
Решение 3: requestAnimationFrame для плавной анимации
Для работы с анимацией используйте requestAnimationFrame (синхронизируется с частотой обновления монитора):
// НЕПРАВИЛЬНО - может блокировать
function animateBad() {
const element = document.getElementById('box');
let position = 0;
setInterval(() => {
position += 10;
element.style.left = position + 'px';
}, 16); // ~60 FPS
}
// ПРАВИЛЬНО - синхронизируется с браузером
function animateGood() {
const element = document.getElementById('box');
let position = 0;
function animate() {
position += 10;
if (position < 500) {
element.style.left = position + 'px';
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
}
Решение 4: Debounce и Throttle для обработчиков событий
Ограничьте количество вызовов функции:
// Debounce - вызвать ТОЛЬКО после паузы
function debounce(func, delay) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), delay);
};
}
// Throttle - вызывать НЕ ЧАЩЕ чем каждые N миллисекунд
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Использование
input.addEventListener('input', debounce((e) => {
// API запрос будет отправлен только ПОСЛЕ того как пользователь перестанет печатать
searchAPI(e.target.value);
}, 300));
window.addEventListener('resize', throttle(() => {
// Обновлять layout не чаще чем каждые 250ms
updateLayout();
}, 250));
Решение 5: Batch обновления в React
В React используйте batch updates для оптимизации рендеров:
function Component() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
const handleClick = () => {
// React автоматически батчит эти обновления
setCount(count + 1);
setText('updated');
// Один рендер вместо двух!
};
// Для асинхронных операций используйте flushSync
const handleAsync = async () => {
const data = await fetchData();
// flushSync гарантирует синхронное обновление
flushSync(() => setCount(data.count));
};
return <button onClick={handleClick}>Click</button>;
}
Чеклист оптимизации
- Избегайте больших циклов в главном потоке
- Используйте Web Workers для тяжелых вычислений
- Разбивайте работу с setTimeout/requestAnimationFrame
- Применяйте debounce/throttle на обработчики событий
- Используйте batch updates в React
- Профилируйте с помощью DevTools Performance tab
- Не забывайте про асинхронные операции (fetch, файлы)
Главная идея: если операция занимает больше 16мс, она начинает вредить плавности (при 60 FPS каждый кадр занимает ~16.67ms). Разбивайте такие операции!