setTimeout() в JavaScript: полное руководство

Расставляем таймеры без ущерба производительности

setTimeout() в JavaScript: полное руководство

setTimeout() кажется простой функцией: подожди — и выполни. Но за этой простотой прячется куча нюансов. Почему setTimeout(…, 0) не срабатывает сразу? Почему this вдруг становится undefined? Почему try-catch не ловит ошибку внутри таймера? И как неосторожное обращение с таймерами может повесить весь интерфейс?

В этой статье разберёмся, что такое setTimeout, как он работает, как правильно его использовать и где чаще всего возникают сложности.

Что такое setTimeout()

setTimeout() — это встроенная функция JavaScript, которая позволяет запустить любой код с задержкой во времени. Она не блокирует выполнение скрипта и не останавливает поток — просто откладывает выполнение указанной функции на нужное количество миллисекунд.

По умолчанию JavaScript работает синхронно: выполняет команды сверху вниз, построчно, одна за другой. Но setTimeout() — асинхронный: он ставит задачу в очередь и продолжает выполнение остального кода. Когда заданное время истечёт, то браузер (или движок Node.js) вернётся и выполнит отложенный код.

Проще говоря, когда мы пишем setTimeout, то говорим: «Подожди N миллисекунд, а потом выполни вот эту функцию». Это не «пауза в коде», а именно «поставь это на потом».

Это основа всей отложенной логики: уведомления, анимации, подсказки, отложенные действия, заглушки, дебаунсы, фейковые загрузчики — всё это делается через setTimeout.

Синтаксис и параметры

Функция setTimeout() используется, чтобы запланировать выполнение другой функции через определённое время (в миллисекундах).

Базовый синтаксис выглядит так:

setTimeout(callback, delay, param1, param2)

Здесь мы прописываем:

  • callback — функция, которую нужно выполнить;
  • delay — задержка в миллисекундах (1000 мс = 1 секунда);
  • param1, param2, ... — необязательные аргументы, которые передаются в колбэк.

Параметры функции

Callback — это функция, которая будет вызвана после указанной задержки. Можно передавать как обычную, так и стрелочную функцию:

setTimeout(function () {
console.log('Через 2 секунды');
}, 2000);

Delay — время задержки в миллисекундах. То есть 1000 — это 1 секунда. Можно указать 0, и тогда функция попадёт в конец очереди (как именно это работает, разберём позже).

Дополнительные аргументы — можно передать значения, которые попадут в callback в виде аргументов:

function greet(name) {
  console.log(`Привет, ${name}`);
}
setTimeout(greet, 2000, 'бро'); // Привет, бро (через 2 секунды)

Возвращаемое значение

Когда вы вызываете setTimeout(), он не просто запускает таймер — он ещё и возвращает число. Это timeoutID — уникальный идентификатор таймера, с помощью которого его можно отменить через clearTimeout().

setTimeout(() => {
  console.log("Через 2 секунды");
}, 2000);
// 5
// Через 2 секунды

Вот это 5 и есть ID таймера. Чтобы не видеть ID в консоли, можно сохранить его в переменную:

const id = setTimeout(() => {
  console.log("Через 2 секунды");
}, 2000);

Так в консоли ничего лишнего не появится.

Как работает setTimeout()

Функция setTimeout() не просто отложенный запуск. Это часть event loop — цикла событий в JS, и она ведёт себя по своим правилам. Код срабатывает не раньше чем через указанную задержку, но точный момент зависит от текущей загрузки потока.

Задержка в миллисекундах

SetTimeout() принимает задержку во втором параметре — в миллисекундах. Но даже если вы поставите 0, функция не выполнится мгновенно. Она встанет в очередь и сработает, когда движок освободится.

console.log("Сначала");
setTimeout(() => {
  console.log("setTimeout с 0");
}, 0);
console.log("Потом");
// Сначала
// Потом
// setTimeout с 0

timeoutID и его использование

Мы уже говорили, что когда вы вызываете setTimeout(), он возвращает уникальный идентификатор — timeoutID. Его можно сохранить в переменную, и в дальнейшем использовать:

const timerId = setTimeout(() => {
  console.log("Прошло 5 секунд");
}, 5000);

timeoutID — уникальный идентификатор таймера, и браузер не даст два одинаковых ID одновременно. Идентификатор живёт в пределах одного окружения — одного окна или воркера. И как только таймер выполнится или отменится, ID может быть переиспользован системой для нового таймера (но не раньше).

Отмена setTimeout с помощью clearTimeout()

Если вы передумали или не хотите, чтобы функция выполнялась, — отмените таймер через clearTimeout(timeoutID):

const id = setTimeout(() => {
  console.log('Я не должен выполниться!');
}, 5000);
clearTimeout(id); // И всё, никаких выполнений

Если передать несуществующий ID, ничего страшного не произойдёт. Ошибки не будет, clearTimeout() просто ничего не сделает. Это удобно, если не уверены, есть ли у вас активный таймер.

👉 setTimeout() и setInterval() используют один и тот же пул ID. Теоретически можно отменить интервал через clearTimeout(), но лучше не хулиганить и использовать правильную пару clearInterval().

Использование setTimeout в реальных проектах

setTimeout() используется в живых интерфейсах, при взаимодействии с пользователем, для отложенных действий, а иногда и для обхода странностей браузеров. Разберёмся на примерах.

Пример: запланировать выполнение функции

Допустим, мы хотим показать на сайте рекламный баннер или приветствие и убрать его через пару секунд. Делается это так:

// Ждём полной загрузки HTML-документа (DOM-дерева)
window.addEventListener("DOMContentLoaded", () => {
  // Находим элемент баннера по классу
  const banner = document.querySelector(".promo-banner");
  // Показываем баннер (если он скрыт стилями по умолчанию)
  banner.style.display = "block";
  // Через 2 секунды (2000 миллисекунд) скрываем баннер
  setTimeout(() => {
    banner.style.display = "none";
  }, 2000);
});

Такой приём часто используют для временных сообщений, баннеров или экранов-заставок.

Пример: использование аргументов с setTimeout()

Допустим, после входа на сайт мы хотим не сразу, а с небольшой задержкой показать пользователю персональное приветствие. Это может выглядеть так:

// Функция, которая вставляет имя пользователя в приветствие и делает блок видимым
function showGreeting(name) {
  const msg = document.querySelector(".greeting");
  msg.textContent = `Привет, ${name}!`;
  // допустим, у блока по умолчанию opacity: 0
  msg.style.opacity = 1; 
}
// Откладываем вызов функции на 1,5 секунды и передаём аргумент "Джеймс"
setTimeout(showGreeting, 1500, "Джеймс Сандерленд");

Функция setTimeout ждёт 1,5 секунды, после этого вызывает showGreeting(), передавая туда строку с именем пользователя, функция обновляет содержимое блока .greeting и делает его видимым.

Хорошо работает, когда нужно сделать интерфейс чуть дружелюбнее — без резких появлений.

Пример: передача строки вместо функции

у setTimeout() есть альтернативный синтаксис: вместо функции можно передать строку с кодом. Она будет скомпилирована и выполнена, когда сработает таймер:

setTimeout("alert('Опасненько')", 1000);

По сути, это работает точно так же, как eval() — а значит, несёт те же риски. eval() — это функция, которая берёт строку и выполняет её как код. Очень мощная штука, но с ней легко запустить в проект дичь, особенно если в строке окажется пользовательский ввод. Это потенциальная дыра в безопасности и источник XSS-атак.

⚠️ Поэтому, если нужно запланировать действие — передавайте функцию, а не строку.

Пример: задержка с нулевым значением

Иногда setTimeout(fn, 0) используют, чтобы дать браузеру отрисовать изменения, прежде чем выполнять какую-то тяжёлую или блокирующую операцию.

Допустим, у нас есть кнопка, по нажатию на которую показывается индикатор загрузки, а потом срабатывает какая-то тяжёлая функция:

button.addEventListener("click", () => {
  loader.style.display = "block"; // Показываем лоадер
  // Плохо: тяжёлая функция блокирует отрисовку лоадера
  doHeavyStuff(); 
});

В этом случае loader может не успеть появиться на экране — потому что движок JavaScript сразу прыгнет в doHeavyStuff и браузер даже не дойдёт до отрисовки. Чтобы этого избежать, используем setTimeout с задержкой 0:

button.addEventListener("click", () => {
  // Показываем загрузчик
  loader.style.display = "block"; 
  // Даём движку отрисовать загрузчик, а потом запускаем тяжёлый код
  setTimeout(() => {
    doHeavyStuff(); 
  }, 0);
});

Теперь всё работает как надо: сначала сработает индикатор загрузки, а только потом запустится сложная операция.

Пример: try-catch в setTimeout()

Многие интуитивно пытаются ловить ошибки внутри setTimeout вот так:

try {
  setTimeout(() => {
    throw new Error("Что-то пошло не так");
  }, 1000);
} catch (err) {
  console.log("Не отловит ", err.message);
}

Но так ничего не выйдет, поскольку setTimeout асинхронный. Ошибка произойдёт уже после того, как try-catch отработал и очистился стек вызовов.

Чтобы правильно ловить ошибки внутри setTimeout, нужно обернуть код внутри самой функции в try...catch:

setTimeout(() => {
  try {
    // потенциально опасный код
    throw new Error("Ошибка внутри таймера");
  } catch (err) {
    console.error("Поймали внутри setTimeout:", err.message);
  }
}, 1000);

Такой приём особенно полезен, если вы запускаете в setTimeout нестабильный код или обрабатываете пользовательские данные.

Пример: this в setTimeout()

В JavaScript значение this зависит от того, как вызвана функция. И это часто становится ловушкой, особенно в связке с setTimeout.

Допустим, вы пишете компонент, который должен показать сообщение через пару секунд:

const popup = {
  message: "Привет, я раздражающее окно!",
  show() {
    // Здесь обычная функция теряет контекст
    setTimeout(function () {
      // undefined
      console.log(this.message);
    }, 2000);
  },
};
popup.show();

Почему this.message не сработало? Потому что внутри setTimeout обычная функция создаёт свой контекст. А значит, this указывает не на popup, а на глобальный объект (window в браузере).

Чтобы исправить это, то внутри setTimeout при обращении к this используем стрелочные функции — это и современно, и безопасно. Стрелочные функции не имеют своего this, а берут его из внешнего контекста.

const popup = {
  message: "Привет, я раздражающее окно!",
  show() {
    setTimeout(() => {
      console.log(this.message); // Всё работает!
    }, 2000);
  },
};
popup.show();

Есть и другие способы, но обычно хватает стрелочных функций. 

Практический пример: дебаунс

В реальных проектах не всегда нужно сразу выполнять функцию при каждом действии пользователя. Если он вводит текст в поисковую строку, то отправлять запрос к серверу на каждую нажатую клавишу плохо.

В таких ситуациях используют дебаунс (debounce) — приём, при котором выполнение функции откладывается, пока пользователь не прекратит активные действия (печатать или скроллить).

Это удобно, когда у нас есть автокомплит в поиске, фильтрация товаров по вводу, ресайз окна и скроллы (чтобы не дёргать логику на каждое движение).

Как работает debounce:

  • принимает другую функцию и таймаут (например, 300 мс);
  • откладывает её выполнение;
  • если функция вызывается повторно до истечения таймаута — старый вызов отменяется.

В результате функция срабатывает только один раз и только после того, как пользователь остановится.

function debounce(fn, delay) {
 // хранит ID текущего таймера
  let timeoutId; 
  // возвращаем новую функцию-обёртку
  return function (...args) {
    // каждый раз при вызове отменяем предыдущий таймер
    clearTimeout(timeoutId);
    // запускаем новый таймер
    timeoutId = setTimeout(() => {
      // вызываем оригинальную функцию с теми же аргументами и сохранённым контекстом
      fn.apply(this, args);
    // задержка перед выполнением (в миллисекундах)
    }, delay); 
  };
}

В итоге у нас снижается нагрузка на сервер и улучшается UX — становится меньше дёрганий и лагов.

Сравнение setTimeout и setInterval

Обе функции позволяют запускать код с задержкой. Но работают они по-разному:

  • setTimeout(fn, delay) — выполнит fn один раз через указанное время.
  • setInterval(fn, delay) — будет выполнять fn периодически, каждые delay миллисекунд, пока не остановим вручную.

Кажется, просто. Но в реальных проектах setInterval может подкинуть сюрпризов. Разберём пару примеров.

Во вкладках не в фокусе таймеры замедляются

Если пользователь свернёт вкладку, браузер начнёт экономить ресурсы: setInterval будет срабатывать реже, а иногда пауза может быть в разы длиннее, чем ожидалось.

Например, вы показываете таймер до конца акции:

setInterval(() => {
  secondsLeft--;
  updateBanner(secondsLeft);
}, 1000);

В фоновом режиме secondsLeft будет уменьшаться нерегулярно — и по возвращении пользователь может увидеть, что таймер сломан. Поэтому вместо этого лучше использовать реальную дату и время.

setInterval не ждёт завершения функции

Допустим, нам по какой-то причине нужно обращаться к серверу каждую секунду:

setInterval(() => {
 // запрос к серверу
  fetch('/api/update'); 
}, 1000);

Проблема в том, что если сервер вдруг начнёт отвечать дольше (например, 1,5 секунды), то новые вызовы fetch() начнут накладываться друг на друга. Вы ещё не получили ответ — а уже отправили следующий запрос. Так накапливаются «хвосты» запросов, и сайт начинает тормозить.

Поэтому лучше использовать setTimeout в цепочке, чтобы следующий вызов происходил только после завершения предыдущего:

function poll() {
  fetch('/api/update')
    .finally(() => {
    // запланируем новый вызов через секунду
      setTimeout(poll, 1000); 
    });
}
poll();

В критичных сценариях вообще лучше избегать setInterval. Если важна точность или стабильность (отсчёт времени, опрос сервера, анимация), setTimeout даст больше контроля.

Нюансы использования setTimeout

На первый взгляд кажется, что setTimeout — простая и удобная штука. Он не блокирует поток, позволяет делать паузы и откладывать действия. Но за всей этой простотой стоит очередь задач, приоритеты и ловушки производительности. 

Разберём, как это работает.

Асинхронность и синхронность

Как мы уже говорили, JavaScript однопоточный: код исполняется сверху вниз, строка за строкой. setTimeout() работает иначе: он ставит задачу в очередь и не ждёт её выполнения.

console.log("1");
setTimeout(() => console.log("2"), 0);
console.log("3");

Даже с задержкой 0 таймер сработает после основного потока, потому что setTimeout — это макрозадача. Он попадёт в очередь и будет выполнен, когда стек вызовов освободится.

Это важно, если вам нужно отложить тяжёлую логику, чтобы сначала всё отрисовалось и если мы хотим дать интерфейсу «отдышаться» перед сложной операцией.

Микрозадачи и макрозадачи

Когда мы используем setTimeout, то добавляем задачу в очередь макрозадач. Это значит, что браузер поставит нашу функцию в очередь и выполнит её только после того, как:

  • завершится основной код (тот, что выполняется строка за строкой);
  • отработают все микрозадачи.

Микрозадачи — это особый тип асинхронных операций, которые исполняются раньше, чем макрозадачи. К ним относятся, например, метод .then() у промисов, async/await (под капотом они тоже используют промисы) и методы вроде queueMicrotask(). Мы подробно разбирали, как работает Event Loop, микрозадачи и макрозадачи, в статье про async/await в JavaScript. Обязательно почитайте, если ещё не видели.

Пример:

setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("promise"));

Хотя setTimeout задан с нулевой задержкой, первым выполнится промис, поскольку он попадает в микрозадачи, а у них выше приоритет.

👉 setTimeout не гарантирует точную задержку — он только говорит «выполни, когда сможешь, но не раньше чем через X мс». А вот микрозадачи браузер старается выполнить сразу после текущего кода.

Проблемы с производительностью

Рассмотрим пару основных примеров.

Утечки памяти. Если вы создаёте таймер, но не отменяете его, когда он уже не нужен, — он может продолжать жить в памяти, даже если связанный с ним DOM-элемент уже удалён. Особенно актуально для одностраничных приложений и динамических компонентов (в React или Vue).

const timeoutId = setTimeout(() => {
  doSomething(); // а элемента на странице уже нет
}, 5000);
// если не вызвать clearTimeout(timeoutId), колбэк всё равно сработает

Всегда вызывайте clearTimeout, если компонент удаляется или таймер больше не нужен. Это освободит ресурсы и предотвратит неожиданное поведение.

Перегрузка таймерами. Если вы ставите setTimeout или setInterval в цикле или запускаете их в ответ на частые события (например, события scroll или mousemove), можно легко создать сотни таймеров, которые браузер попытается отработать одновременно. Это приводит к замираниям интерфейса, ненужной нагрузке на процессор и снижению отзывчивости страницы.

Поэтому используйте дебаунс для «заглушки» частых вызовов:

// Применяем debounce для события scroll
window.addEventListener(
  "scroll",
  debounce(() => {
    // Эта функция не будет вызываться на каждый пиксель прокрутки —
    // а только когда прокрутка остановится хотя бы на 300 мс
    console.log("Обработка scroll");
  }, 300)
);

Это спасёт и интерфейс, и нервы пользователей.

Бонус для читателей

Если вам интересно погрузиться в мир ИТ и при этом немного сэкономить, держите наш промокод на курсы Практикума. Он даст вам скидку при оплате, поможет с льготной ипотекой и даст безлимит на маркетплейсах. Ладно, окей, это просто скидка, без остального, но хорошая. 

Вам слово

Приходите к нам в соцсети поделиться своим мнением о статье и почитать, что пишут другие. А ещё там выходит дополнительный контент, которого нет на сайте — шпаргалки, опросы и разная дурка. В общем, вот тележка, вот ВК — велком!

Обложка:

Алексей Сухов

Корректор:

Александр Зубов

Вёрстка:

Егор Степанов

Соцсети:

Юлия Зубарева

Вам может быть интересно
easy
[anycomment]
Exit mobile version