Уявіть інтернет-магазин без ознак життя. Відвідувач не бачить, що інші купують або переглядають товари — йде з відчуттям порожнечі. Один з наших клієнтів зіткнувся з падінням конверсії на 20% після редизайну. Аналіз показав: нова версія не відображала активність інших користувачів. Ми впровадили стрічку активності в реальному часі. Результат — конверсія повернулася до попереднього рівня і зросла на 12%.
Ми будуємо Live Activity Feed — безперервний потік подій: «Іван купив товар», «Марія залишила відгук», «5 осіб зараз дивляться». Це створює соціальний доказ і утримує відвідувачів. Після впровадження конверсія стабільно зростає на 10–15%. За плечима команди багаторічний досвід роботи з високонавантаженими системами та понад 50 успішних проєктів.
Як стрічка активності вирішує проблему соціального доказу?
Стрічка показує реальні дії користувачів у реальному часі, створюючи ефект присутності. Відвідувач бачить, що товар затребуваний, і приймає рішення швидше. Механізм дедуплікації та анонімізації робить потік природним, без спам-ефекту.
Проблеми, які вирішуємо
- Порожня сторінка без життя. Потрібен механізм миттєвого відображення активності. Без нього користувачі не відчувають довіри.
- Спам і накрутка. Без фільтрації стрічка стає каналом для ботів. Впроваджуємо дедуплікацію, cooldown 30 секунд та анонімізацію імен.
- Продуктивність при піках. SSE на Redis pub/sub витримує 10 000+ одночасних підписників без втрати подій. Час доставки — менше 500 мс.
Як ми це робимо: стек та архітектура
Використовуємо зв'язку: Node.js (Express) на бекенді, Redis для pub/sub, React 18 з EventSource на фронті. Цей підхід легший і надійніший за WebSocket для односторонньої трансляції. Згідно з Server-Sent Events, SSE працює поверх стандартного HTTP, автоматично перепідключається при обриві.
Серверна генерація подій
class ActivityFeedService {
async publishActivity(event: ActivityEvent): Promise<void> {
// Зберегти в БД для нових відвідувачів
await this.activityRepo.create(event);
// Опублікувати в Redis для live-підписників
await this.redis.publish('activity:feed', JSON.stringify(event));
// Очистити старі події (зберігати 24 години)
await this.activityRepo.deleteOlderThan(24 * 60 * 60 * 1000);
}
}
// Інтеграція з бізнес-логікою
orderService.on('order:created', async (order) => {
const product = await productRepo.findById(order.items[0].productId);
await activityFeed.publishActivity({
type: 'purchase',
text: `${anonymizeName(order.customerName)} купив «${product.name}»`,
location: order.customerCity,
timestamp: new Date(),
metadata: { productId: product.id }
});
});
SSE Feed Endpoint
Приклад реалізації SSE endpoint
app.get('/api/activity/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Відправити останні 10 подій
activityRepo.findRecent(10).then(events => {
res.write(`event: init\ndata: ${JSON.stringify(events)}\n\n`);
});
// Підписатися на нові
const subscriber = redis.duplicate();
subscriber.subscribe('activity:feed');
subscriber.on('message', (_, message) => {
res.write(`event: activity\ndata: ${message}\n\n`);
});
const heartbeat = setInterval(() => res.write(':ping\n\n'), 20000);
req.on('close', () => {
clearInterval(heartbeat);
subscriber.unsubscribe();
subscriber.quit();
});
});
React компонент
function ActivityFeed() {
const [activities, setActivities] = useState<Activity[]>([]);
useEffect(() => {
const source = new EventSource('/api/activity/stream');
source.addEventListener('init', (e) => {
setActivities(JSON.parse(e.data));
});
source.addEventListener('activity', (e) => {
const activity = JSON.parse(e.data);
setActivities(prev => [activity, ...prev].slice(0, 20));
});
return () => source.close();
}, []);
return (
<div className="activity-feed">
{activities.map((activity, i) => (
<ActivityItem key={activity.id} activity={activity}
style={{ opacity: Math.max(0.3, 1 - i * 0.05) }} />
))}
</div>
);
}
function ActivityItem({ activity, style }) {
const icons = { purchase: '🛍', review: '⭐', view: '👁' };
return (
<div className="activity-item" style={style}>
<span className="icon">{icons[activity.type]}</span>
<span className="text">{activity.text}</span>
<span className="time">{formatRelativeTime(activity.timestamp)}</span>
</div>
);
}
Чому SSE швидше за WebSocket для цієї задачі?
SSE (Server-Sent Events) використовує одне HTTP-з'єднання без додаткових протоколів. Автоматичне перепідключення вбудоване — при обриві браузер сам відновлює стрім. WebSocket вимагає рукостискання та підтримки з'єднання, що є надлишковим для одностороннього потоку. Порівняння:
| Характеристика | SSE | WebSocket |
|---|---|---|
| Напрямок | Сервер → Клієнт | Двостороннє |
| Протокол | HTTP | Власний ws:// |
| Автоматичне перепідключення | Так | Потребує коду |
| Простота реалізації | Висока | Середня |
| Підтримка браузерами | Всі сучасні | Всі сучасні |
Забезпечення реалістичності стрічки
Алгоритм дедуплікації запобігає повтору однакових подій підряд. Ми анонімізуємо імена (перший символ + *), вносимо випадкову затримку 0–5 секунд, обмежуємо частоту — не більше однієї події за 2 секунди на користувача. Крім того, зберігаємо історію за 24 години, щоб новий відвідувач бачив не порожній екран.
// Дедуплікація — не показувати однакові події підряд
const recentTexts = new Set<string>();
async function shouldPublish(event: ActivityEvent): Promise<boolean> {
const key = `${event.type}:${event.metadata?.productId}`;
if (recentTexts.has(key)) return false;
recentTexts.add(key);
setTimeout(() => recentTexts.delete(key), 30 * 1000); // 30 сек cooldown
return true;
}
Процес роботи
- Аналітика: Визначаємо типи подій (покупки, відгуки, перегляди) та частоту оновлення.
- Проектування: Проектуємо схему Redis, endpoint SSE, компоненти React.
- Реалізація: Пишемо сервіс публікації, стрімінг, клієнтську частину.
- Тестування: Навантажувальне тестування (до 10k підписників), перевірка дедуплікації.
- Деплой: Налаштування Nginx для SSE, моніторинг Redis, документація.
| Етап | Тривалість |
|---|---|
| Аналітика та проектування | 1 день |
| Розробка бекенду (Redis + SSE) | 1–2 дні |
| Інтеграція React компонента | 1 день |
| Тестування та деплой | 1 день |
| Разом | 3–5 днів |
Терміни орієнтовно
Базова інтеграція — 3–5 днів. Якщо потрібна складна логіка (інтеграція з CRM, кастомні фільтри) — до 10 днів. Точна вартість визначається після аналізу.
Що входить в роботу
- React-компонент ActivityFeed з анімацією затухання
- Node.js SSE endpoint з Redis pub/sub
- Антиспам та дедуплікація
- Налаштування Nginx для довгих з'єднань
- Документація з інтеграції нових подій
- Навчання команди (1 година онлайн)
- Можлива економія до 30% часу при використанні готових компонентів
Типові помилки при реалізації
- Не налаштований heartbeat. Без ping-повідомлень кожні 20 с балансувальники розривають з'єднання.
- Відсутність init-події. Новий відвідувач бачить порожній список, доки не з'явиться перша подія.
- Витік підписників. Якщо не відписуватися при закритті сторінки, Redis накопичує мертві канали.
Ми гарантуємо стабільну роботу стрічки під навантаженням і повну документацію. Зв'яжіться з нами — обговоримо ваш проєкт. Отримайте консультацію щодо впровадження стрічки активності. Оцінимо задачу і запропонуємо оптимальне рішення.







