← Назад к вопросам

Как заблокировать поток в JS?

2.0 Middle🔥 191 комментариев
#JavaScript Core#Браузер и сетевые технологии

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Блокирование потока в JavaScript: синхронный код и event loop

JavaScript однопоточный язык, который работает на основе Event Loop. "Блокирование потока" означает выполнение длительной синхронной операции, которая останавливает обработку других задач. Это критически важная концепция для понимания производительности.

Как работает Event Loop

┌─────────────────────────────────┐
│   Call Stack (Main Thread)      │ <- Выполняет текущий код
├─────────────────────────────────┤
│   Event Loop                    │ <- Проверяет очереди
├─────────────────────────────────┤
│   Callback Queue (Macrotasks)   │ <- setTimeout, setInterval
│   Microtask Queue (Promises)    │ <- Promise.then(), async/await
└─────────────────────────────────┘

Пример блокирования потока

// НЕПРАВИЛЬНО: синхронный код блокирует поток
function expensiveCalculation() {
  let sum = 0;
  for (let i = 0; i < 10_000_000_000; i++) {
    sum += i;
  }
  return sum;
}

console.log('Start');
expensiveCalculation(); // Это займет ~5-10 секунд!
console.log('End'); // Выполнится только после завершения

// Во время вычисления UI ЗАМОРОЖЕН:
// - Не работают клики
// - Не скроллится страница
// - Не воспроизводится видео

Решение 1: Разделение на chunks с setTimeout

// ПРАВИЛЬНО: асинхронное выполнение
function expensiveCalculationAsync() {
  let sum = 0;
  let i = 0;
  const chunkSize = 100_000_000;

  return new Promise((resolve) => {
    function processChunk() {
      const endI = Math.min(i + chunkSize, 10_000_000_000);
      
      for (; i < endI; i++) {
        sum += i;
      }

      if (i < 10_000_000_000) {
        // Отдаём контроль браузеру перед следующим chunk
        setTimeout(processChunk, 0);
      } else {
        resolve(sum);
      }
    }

    processChunk();
  });
}

console.log('Start');
expensiveCalculationAsync().then(result => {
  console.log('Result:', result);
  console.log('End');
});
// Вывод сразу:
// Start
// ... UI остаётся отзывчивым ...
// Result: 49999999950000000
// End

Решение 2: Web Workers (рекомендуется для тяжелых вычислений)

Web Workers запускают код в отдельном потоке, не блокируя основной поток.

// main.js
console.log('Start');

const worker = new Worker('worker.js');
worker.postMessage({ n: 10_000_000_000 });

worker.onmessage = (event) => {
  console.log('Result:', event.data);
  console.log('End'); // Выполнится когда worker закончит
};

// UI остаётся полностью отзывчивым!
// worker.js
self.onmessage = (event) => {
  const { n } = event.data;
  let sum = 0;

  // Это выполняется в отдельном потоке
  for (let i = 0; i < n; i++) {
    sum += i;
  }

  // Отправляем результат обратно
  self.postMessage(sum);
};

Использование в React:

// hooks/useExpensiveCalculation.ts
export function useExpensiveCalculation(data: number[]) {
  const [result, setResult] = useState<number | null>(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    
    const worker = new Worker(new URL('../workers/calculation.js', import.meta.url), {
      type: 'module'
    });

    worker.postMessage(data);

    worker.onmessage = (event) => {
      setResult(event.data);
      setLoading(false);
    };

    return () => worker.terminate();
  }, [data]);

  return { result, loading };
}

// Использование
export function DataAnalysis() {
  const data = Array.from({ length: 1_000_000 }, (_, i) => i);
  const { result, loading } = useExpensiveCalculation(data);

  if (loading) return <div>Calculating...</div>;
  return <div>Result: {result}</div>;
}

Решение 3: requestAnimationFrame для визуальных обновлений

// Обновление DOM частями
function updateLargeList(items) {
  let index = 0;
  const container = document.getElementById('list');

  function renderChunk() {
    const end = Math.min(index + 100, items.length);

    for (; index < end; index++) {
      const li = document.createElement('li');
      li.textContent = items[index];
      container.appendChild(li);
    }

    if (index < items.length) {
      // Отдаём контроль браузеру для reflow/repaint
      requestAnimationFrame(renderChunk);
    }
  }

  requestAnimationFrame(renderChunk);
}

updateLargeList(new Array(10_000).fill(0).map((_, i) => `Item ${i}`));

Решение 4: Debouncing / Throttling для обработчиков событий

// Блокирующий обработчик
window.addEventListener('scroll', () => {
  // Это выполнится СОТНИ раз в секунду!
  expensiveCalculation();
});

// Правильно: throttle
function throttle(func, limit) {
  let inThrottle;
  return function (...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

window.addEventListener('scroll', throttle(() => {
  expensiveCalculation();
}, 1000)); // Максимум раз в секунду

Как определить блокирование потока

// Инструменты отладки
console.time('calculation');
expensiveCalculation();
console.timeEnd('calculation'); // calculation: 5234.12ms

// Performance API
const start = performance.now();
expensiveCalculation();
const end = performance.now();
console.log(`Took ${end - start}ms`);

// Chrome DevTools: Performance tab
// - Красные блоки = Main Thread заблокирован
// - Ищи длительные Task в flame chart

Пример React компонента с блокированием

// Неправильно
export function BadComponent() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    // Это блокирует поток на 5 секунд!
    const sum = Array.from({ length: 10_000_000_000 }, (_, i) => i)
      .reduce((a, b) => a + b, 0);
    
    setCount(sum);
  };

  return <button onClick={handleClick}>Calculate ({count})</button>;
}

// Правильно
export function GoodComponent() {
  const [count, setCount] = useState(0);
  const [loading, setLoading] = useState(false);

  const handleClick = async () => {
    setLoading(true);
    
    // Отдаём контроль браузеру перед вычислением
    await new Promise(resolve => setTimeout(resolve, 0));

    const sum = await new Promise<number>(resolve => {
      const worker = new Worker('calc.js');
      worker.postMessage(10_000_000_000);
      worker.onmessage = (e) => resolve(e.data);
    });

    setCount(sum);
    setLoading(false);
  };

  return (
    <button onClick={handleClick} disabled={loading}>
      {loading ? 'Calculating...' : `Calculate (${count})`}
    </button>
  );
}

Таблица способов избежать блокирования

СпособКогда использоватьПлюсыМинусы
setTimeoutНебольшие операцииПростоМедленно
Web WorkerТяжелые вычисленияНастоящий многопотокСложнее, overhead
requestAnimationFrameDOM обновленияСинхронизация с браузеромТолько для анимаций
Debounce/ThrottleОбработчики событийЭкономит CPUЗадержка в обработке
async/awaitАсинхронные операцииЧитаемоНе решает проблему

Итог

Блокирование потока в JavaScript происходит когда:

  • Выполняется длительный синхронный код
  • Воспроизводится тяжелый JavaScript
  • Выполняются сложные DOM операции

Как избежать:

  1. Разделяй на chunks с setTimeout
  2. Используй Web Workers для тяжелых вычислений
  3. requestAnimationFrame для DOM обновлений
  4. Профилируй с Chrome DevTools Performance tab
  5. Избегай синхронных циклов в обработчиках событий

Это критично для создания отзывчивого пользовательского интерфейса.

Как заблокировать поток в JS? | PrepBro