Почему функция в setTimeout может вызваться позже, чем указано время задержки в Node.js?

Ответ

В Node.js таймеры, установленные через setTimeout(), обрабатываются на фазе Timers цикла событий (Event Loop). Задержка выполнения колбэка возникает из-за того, что Event Loop — однопоточный, и фаза Timers наступает только после завершения текущего синхронного кода и других фаз (например, Pending callbacks, Poll).

Основные причины задержки:

  1. Блокировка основного потока долгими синхронными операциями.
  2. Выполнение других фаз Event Loop, которые заняли больше времени.
  3. Загрузка процессора другими задачами в системе.

Пример блокировки Event Loop:

console.log('Start');
setTimeout(() => console.log('Timeout callback'), 100);

// Долгая синхронная операция блокирует Event Loop
const start = Date.now();
while (Date.now() - start < 2000) { /* Ждём 2 секунды */ }

console.log('End after blocking loop');
// Вывод:
// Start
// End after blocking loop
// Timeout callback (выполнится через ~2000 мс, а не 100 мс)

Важно: setTimeout(fn, delay) гарантирует, что функция fn будет вызвана не раньше, чем через delay миллисекунд, но не гарантирует точное время. Для точного измерения времени лучше использовать performance.now() или Date.now() внутри колбэка.

Минимальная задержка: В Node.js есть минимальная задержка (~1 мс), но она может увеличиваться в зависимости от нагрузки.

Ответ 18+ 🔞

Давай разберем эту тему, как будто объясняю другу за пивом, а не на скучной лекции. Представь, что Event Loop — это такой охуенно важный бармен в баре твоего кода. Он один, и он делает всё: наливает, моет стаканы, выгоняет пьяных. И у него есть строгий порядок действий — фазы, так сказать.

Ты говоришь ему: «Чувак, через 100 миллисекунд подойди и скажи „Таймаут“». Это твой setTimeout. Бармен кивает: «Окей, записал». Но тут к нему подваливает какой-то мудак с заказом на 100 коктейлей (это твой долгий синхронный цикл while). И пока бармен их мешает, он нихуя не может отойти. Прошло и 100 мс, и 200, а он всё мешает. Вот тебе и задержка. Таймер сработает только тогда, когда бармен освободится и дойдёт до пункта «проверить таймеры» в своём списке дел.

Ёпта, основные причины, почему твой таймер может проебаться:

  1. Главный поток в говне. Если ты нагрузил его какой-нибудь ебанутой синхронной хуйнёй (типа перебора огромного массива или, как в примере, тупого цикла), то Event Loop просто встанет колом. Он однопоточный, ему некуда деваться.
  2. Другие фазы затянулись. Может, перед фазой таймеров были другие важные дела — ввод/вывод, какие-то колбэки. Пока они не закончатся, до таймеров очередь не дойдёт.
  3. Система тупит. Компьютер твой, прости господи, древний, как говно мамонта, и процессор забит чем-то другим. Тогда всё будет тормозить, не только твой таймер.

Смотри, как это выглядит в коде, прям наглядный пиздец:

console.log('Стартуем!');
setTimeout(() => console.log('Колбэк таймера'), 100);

// А вот сейчас начнётся адская блокировка
const start = Date.now();
while (Date.now() - start < 2000) {
    // Просто сидим и нихуя не делаем 2 секунды
}

console.log('Цикл закончился, выдохнули');
// Что выведет:
// Стартуем!
// Цикл закончился, выдохнули
// Колбэк таймера (выполнится через ~2000 мс, а не обещанные 100)

Видишь? Таймер поставили на 100 мс, но колбэк выстрелил только через 2 секунды, потому что весь этот цикл — это как раз тот мудак, который занял бармена.

Запомни главное, как «Отче наш»: setTimeout(fn, delay) — это не команда «выполни ровно через delay». Это просьба «выполни не раньше, чем через delay». Может быть и позже, иногда на овердохуища позже. Если тебе нужна точность до миллисекунд (для замеров производительности, например), то не верь задержке, а используй performance.now() прямо внутри самой функции.

И да, в Node.js есть минимальная задержка, около 1 мс, но на неё тоже особо не надейся — если система под нагрузкой, она легко превратится в «хуй с горы».