Как выполнить тяжёлую задачу в JavaScript, не блокируя интерфейс?
Производительность в JS
JavaScript в браузере выполняется в одном потоке, поэтому долгий цикл блокирует отрисовку и обработку событий: страница «замирает». Есть три способа не блокировать интерфейс: разбить работу на части, отдать её другому потоку или отложить до простоя.
Разбиение на части
Работу делят на порции и между ними возвращают управление браузеру:
async function processAll(items) {
const chunk = 200;
for (let i = 0; i < items.length; i += chunk) {
items.slice(i, i + chunk).forEach(handle);
await new Promise(resolve => setTimeout(resolve, 0)); // даём отрисоваться
}
}Вместо setTimeout можно использовать scheduler.yield() (где доступен) или requestAnimationFrame.
Web Worker
Вычисления без доступа к DOM переносят в отдельный поток:
const worker = new Worker('worker.js');
worker.postMessage(data);
worker.onmessage = e => render(e.data);Основной поток остаётся свободным, но данные копируются (structured clone) — для больших массивов используют Transferable.
requestIdleCallback
Низкоприоритетную работу запускают, когда браузер простаивает:
requestIdleCallback(deadline => {
while (deadline.timeRemaining() > 0 && queue.length) run(queue.shift());
});Коротко для собеседования
Долгая синхронная задача блокирует единственный поток. Её режут на порции с паузами для отрисовки, выносят в Web Worker или выполняют в requestIdleCallback.