Таймеры и countdown

Работа с таймерами в приложениях на JavaScript почти всегда выходит за рамки простого setTimeout. При построении countdown-систем, уведомлений, игровых механик и интерфейсов реального времени требуется учитывать дрейф таймеров, смену часовых поясов, локализацию времени и точные вычисления интервалов. Библиотека Luxon решает значительную часть этих проблем, предоставляя строгую модель работы с датой и временем через DateTime, Duration и Interval.


Базовая модель времени в Luxon

В основе работы с таймерами лежат три ключевые сущности:

DateTime — конкретный момент времени Duration — длительность Interval — промежуток между двумя моментами

import { DateTime, Duration, Interval } from "luxon";

Момент времени как точка отсчёта

const start = DateTime.now();

Любой таймер или countdown в конечном итоге привязывается к сравнению текущего времени с целевой точкой.


Простейший таймер на setInterval и Luxon

Классический подход — регулярное сравнение текущего времени с целевым моментом.

import { DateTime } from "luxon";

const target = DateTime.now().plus({ seconds: 30 });

const timer = setInterval(() => {
  const now = DateTime.now();
  const diff = target.diff(now, ["hours", "minutes", "seconds"]);

  console.log(diff.toFormat("hh:mm:ss"));

  if (now >= target) {
    clearInterval(timer);
    console.log("Время вышло");
  }
}, 1000);

Особенность Luxon заключается в том, что разница времени возвращается не числом, а структурированной длительностью, что упрощает форматирование и дальнейшую обработку.


Countdown через Duration

Более точная модель строится через Duration.

import { DateTime } from "luxon";

const target = DateTime.now().plus({ minutes: 5 });

function getRemaining() {
  const now = DateTime.now();
  const duration = target.diff(now);

  return duration.shiftTo("minutes", "seconds");
}

console.log(getRemaining().toObject());

Форматирование оставшегося времени

const remaining = target.diff(DateTime.now());

console.log(
  remaining.toFormat("mm:ss")
);

Метод toFormat позволяет формировать строку без ручных вычислений.


Точность таймеров и проблема дрейфа

setInterval не гарантирует точные интервалы. Luxon используется для компенсации дрейфа.

Корректный countdown с пересчётом времени

const target = DateTime.now().plus({ minutes: 1 });

function tick() {
  const now = DateTime.now();
  const diff = target.diff(now).shiftTo("seconds");

  const seconds = Math.max(0, Math.floor(diff.seconds));

  console.log(seconds);

  if (seconds > 0) {
    setTimeout(tick, 1000);
  }
}

tick();

Здесь используется рекурсивный setTimeout, а Luxon обеспечивает вычисление реального оставшегося времени, а не накопленной погрешности.


Интервальные таймеры через Interval

Interval используется для работы с диапазонами времени.

import { DateTime, Interval } from "luxon";

const start = DateTime.now();
const end = start.plus({ seconds: 10 });

const interval = Interval.fromDateTimes(start, end);

console.log(interval.length("seconds"));

Проверка принадлежности текущего времени

const now = DateTime.now();

if (interval.contains(now)) {
  console.log("Таймер активен");
}

Countdown до конкретной даты

Наиболее частый сценарий — обратный отсчёт до события.

import { DateTime } from "luxon";

const eventTime = DateTime.fromISO("2026-06-01T12:00:00");

function getCountdown() {
  const diff = eventTime.diff(DateTime.now()).shiftTo(
    "days",
    "hours",
    "minutes",
    "seconds"
  );

  return diff.toObject();
}

Вывод в человекочитаемом виде

const d = getCountdown();

console.log(
  `${Math.floor(d.days)}d ${Math.floor(d.hours)}h ${Math.floor(d.minutes)}m`
);

Работа с часовыми поясами в таймерах

Luxon строго учитывает time zone через DateTime.setZone.

import { DateTime } from "luxon";

const local = DateTime.now();
const tokyo = DateTime.now().setZone("Asia/Tokyo");

console.log(local.toISO());
console.log(tokyo.toISO());

Countdown с фиксированной зоной

const target = DateTime.fromISO(
  "2026-06-01T12:00:00",
  { zone: "Europe/Moscow" }
);

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


Сравнение DateTime и UNIX-времени

Luxon позволяет переходить к миллисекундам при необходимости оптимизации:

const nowMillis = DateTime.now().toMillis();

Таймер через миллисекунды

const target = DateTime.now().plus({ seconds: 10 }).toMillis();

setInterval(() => {
  const remaining = target - Date.now();
  console.log(remaining);
}, 1000);

Однако при использовании Luxon сохраняется более высокая точность и читаемость кода.


Стабилизация countdown в UI

При визуализации таймера важно избегать скачков секунд.

import { DateTime } from "luxon";

let lastValue = null;

setInterval(() => {
  const diff = DateTime
    .fromISO("2026-06-01T12:00:00")
    .diff(DateTime.now())
    .shiftTo("seconds");

  const seconds = Math.max(0, Math.floor(diff.seconds));

  if (seconds !== lastValue) {
    lastValue = seconds;
    console.log(seconds);
  }
}, 200);

Частое обновление интервала позволяет сгладить визуальные задержки.


Пауза и возобновление таймера

Luxon упрощает сохранение состояния таймера через пересчёт абсолютной точки.

let target = DateTime.now().plus({ minutes: 2 });

let pausedAt = null;

function pause() {
  pausedAt = DateTime.now();
  target = target.minus(pausedAt.diff(DateTime.now()));
}

function resume() {
  const now = DateTime.now();
  target = now.plus(target.diff(pausedAt));
}

Таймеры с учётом серверного времени

В распределённых системах локальное время может быть ненадёжным. Luxon позволяет нормализовать время через ISO и UTC.

const serverTime = DateTime.fromISO("2026-05-23T10:00:00Z");

const target = serverTime.plus({ minutes: 10 });

const remaining = target.diff(DateTime.utc());

Использование UTC снижает риск расхождений между клиентом и сервером.


Практика: универсальный countdown-движок

import { DateTime } from "luxon";

class Countdown {
  constructor(targetISO) {
    this.target = DateTime.fromISO(targetISO);
    this.timer = null;
  }

  start(callback) {
    this.timer = setInterval(() => {
      const diff = this.target.diff(DateTime.now())
        .shiftTo("hours", "minutes", "seconds");

      callback(diff.toObject());

      if (this.target <= DateTime.now()) {
        this.stop();
      }
    }, 1000);
  }

  stop() {
    clearInterval(this.timer);
  }
}

Динамические интервалы и адаптация частоты обновления

Вместо фиксированного интервала возможна адаптация частоты обновления:

function adaptiveTick(target) {
  const now = DateTime.now();
  const diff = target.diff(now).as("seconds");

  if (diff > 60) {
    setTimeout(() => adaptiveTick(target), 10000);
  } else {
    setTimeout(() => adaptiveTick(target), 1000);
  }
}

Такая схема снижает нагрузку при длительных countdown.


Обработка завершения таймера

const diff = target.diff(DateTime.now()).as("seconds");

if (diff <= 0) {
  // состояние завершения
}

Luxon обеспечивает корректное поведение даже при отрицательных значениях времени.


Форматирование времени для интерфейсов

const formatted = target
  .diff(DateTime.now())
  .shiftTo("days", "hours", "minutes", "seconds")
  .toFormat("d 'д' h 'ч' m 'м' s 'с'");

Такой подход исключает ручные вычисления и снижает вероятность ошибок в UI-логике.


Итоговая модель работы countdown в Luxon

  • фиксированная точка времени через DateTime
  • вычисление разницы через diff
  • преобразование через Duration.shiftTo
  • вывод через toFormat
  • контроль дрейфа через пересчёт абсолютного времени
  • синхронизация через UTC или explicit zone

Эта модель обеспечивает предсказуемое поведение таймеров в любых средах исполнения JavaScript.