Реализация Urgency/Scarcity элементов (таймер, ограниченное количество) на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация Urgency/Scarcity элементов (таймер, ограниченное количество) на сайте
Средняя
от 1 рабочего дня до 3 рабочих дней
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Реализация Urgency/Scarcity элементов (таймер, ограниченное количество) на сайте

Urgency (срочность) и scarcity (дефицит) — классические принципы Чалдини, работающие в e-commerce уже 20 лет. Проблема: большинство реализаций либо технически ненадёжны (таймер сбрасывается при обновлении страницы), либо выглядят как очевидная манипуляция (счётчик «осталось 3 штуки», который никогда не меняется). Ниже — реализация, которая работает честно и технически правильно.

Таймер обратного отсчёта

Требование к надёжности: таймер не должен сбрасываться при обновлении страницы. Нельзя делать это через new Date() + N минут при каждом монтировании компонента.

Правильная схема:

  1. При первом посещении страницы акции создаём запись в Redis с TTL
  2. При последующих посещениях берём оставшееся время из Redis
  3. Для неаутентифицированных — ключ по sessionId из cookie
// Получение или создание таймера сессии
public function getCountdown(Request $request, string $promoCode): array
{
    $sessionId = $request->cookie('session_id') ?? Str::uuid()->toString();
    $key = "countdown:{$promoCode}:{$sessionId}";

    $ttl = Redis::ttl($key);

    if ($ttl <= 0) {
        $duration = 1800; // 30 минут
        Redis::setex($key, $duration, now()->addSeconds($duration)->timestamp);
        $ttl = $duration;
    }

    return [
        'ends_at'     => now()->addSeconds($ttl)->toIso8601String(),
        'session_id'  => $sessionId,
    ];
}

Компонент таймера (React):

const CountdownTimer: React.FC<{ endsAt: string }> = ({ endsAt }) => {
  const [timeLeft, setTimeLeft] = useState(0);

  useEffect(() => {
    const target = new Date(endsAt).getTime();

    const tick = () => {
      const diff = Math.max(0, target - Date.now());
      setTimeLeft(diff);
    };

    tick();
    const interval = setInterval(tick, 1000);
    return () => clearInterval(interval);
  }, [endsAt]);

  const hours   = Math.floor(timeLeft / 3_600_000);
  const minutes = Math.floor((timeLeft % 3_600_000) / 60_000);
  const seconds = Math.floor((timeLeft % 60_000) / 1000);

  if (timeLeft === 0) return <ExpiredBanner />;

  return (
    <div className="countdown" role="timer" aria-live="polite">
      <Digit value={hours}   label="ч" />
      <Digit value={minutes} label="м" />
      <Digit value={seconds} label="с" />
    </div>
  );
};

Компонент Digit добавляет flip-анимацию при смене значения через CSS @keyframes.

Индикатор остатков товара

Показывать реальные остатки из складской системы — лучшая практика. Если остаток ≤ N единиц, показываем предупреждение:

const StockIndicator: React.FC<{ stock: number }> = ({ stock }) => {
  if (stock > 10) return null;

  return (
    <div className={`stock-indicator stock-indicator--${stock <= 3 ? 'critical' : 'low'}`}>
      {stock <= 3
        ? `Последние ${stock} шт.`
        : `Осталось ${stock} шт. — заканчивается`}
    </div>
  );
};

Синхронизация с реальным складом — если используется 1С или Warehouse Management System, остатки подтягиваются через API и кэшируются в Redis на 5 минут:

public function getStockLevel(int $productId): int
{
    return Cache::remember("stock:{$productId}", 300, function () use ($productId) {
        return $this->warehouseApi->getAvailableQuantity($productId);
    });
}

Важно: не показывать точный остаток для очень популярных товаров — это создаёт эффект «стадного поведения» (другие тоже хотят этот товар) и немного усиливает конверсию.

«X человек смотрят прямо сейчас»

Счётчик активных пользователей на странице товара — реальный или приближённый.

Реальная реализация через Redis:

// При загрузке страницы товара
public function trackView(int $productId, string $sessionId): int
{
    $key = "viewers:{$productId}";
    Redis::zadd($key, time(), $sessionId);
    Redis::zremrangebyscore($key, 0, time() - 300); // убираем старше 5 минут
    Redis::expire($key, 600);

    return Redis::zcard($key);
}

Для обновления в реальном времени — либо polling каждые 30 секунд, либо Server-Sent Events:

// SSE endpoint
public function viewersStream(int $productId): StreamedResponse
{
    return response()->stream(function () use ($productId) {
        while (true) {
            $count = $this->viewerService->getCount($productId);
            echo "data: {\"viewers\": {$count}}\n\n";
            ob_flush();
            flush();
            sleep(30);
        }
    }, 200, [
        'Content-Type'  => 'text/event-stream',
        'Cache-Control' => 'no-cache',
    ]);
}
// На клиенте
useEffect(() => {
  const es = new EventSource(`/api/products/${productId}/viewers`);
  es.onmessage = (e) => setViewers(JSON.parse(e.data).viewers);
  return () => es.close();
}, [productId]);

Flash sale (акция на ограниченное время)

Flash sale требует атомарной работы с остатками — без конкурентных записей:

// Lua-скрипт в Redis для атомарного декремента
$script = <<<'LUA'
local key = KEYS[1]
local current = tonumber(redis.call('GET', key) or '0')
if current <= 0 then
    return -1
end
return redis.call('DECR', key)
LUA;

$remaining = Redis::eval($script, 1, "flash_sale:{$saleId}:stock");

if ($remaining < 0) {
    return response()->json(['error' => 'Товар закончился'], 422);
}

«Последний в корзине»

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

{isLastInCart && (
  <Alert variant="warning">
    Этот товар есть в корзинах других покупателей. Оформите заказ, чтобы зарезервировать его.
  </Alert>
)}

Техническая реализация: таблица cart_reservations(product_id, quantity, session_id, expires_at). Резервирование снимается через 30 минут или при оформлении заказа.

Этика и антипаттерны

Urgency-элементы работают, когда они честны. Паттерны, которые вредят репутации:

  • Таймер, который сбрасывается при каждом заходе на страницу
  • «Осталось 2 штуки» на товаре с постоянным наличием
  • Счётчик просматривающих, который рандомно генерируется на клиенте

Эти приёмы краткосрочно повышают конверсию, но долгосрочно разрушают доверие — пользователи замечают несоответствия.

Сроки

Задача Время
Countdown timer (Redis + компонент) 1 день
Индикатор остатков (реальные данные) 0.5 дня
Счётчик просматривающих (SSE) 1 день
Flash sale с Redis-резервированием 1–2 дня

Базовый набор (таймер + остатки): 1.5–2 дня.