Паттерн реального времени

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


Основная идея

Пользователь видит “2 минуты назад” сразу после загрузки. Через минуту без перезагрузки страницы надпись меняется на “3 минуты назад”. Через 57 минут — “час назад”.

timeago.js реализует это через функцию render, которая создаёт внутренние таймеры с адаптивным интервалом:

  • Для дат < 1 минуты: обновление каждые 10-15 секунд.
  • Для дат от 1 минуты до 1 часа: обновление каждую минуту.
  • Для более старых дат: реже.

Базовая реализация

<time datetime="2025-06-01T11:55:00Z">только что</time>
import { render, cancel } from 'timeago.js';
import ru from 'timeago.js/esm/lang/ru';
import { register } from 'timeago.js';

register('ru', ru);

const el = document.querySelector('time[datetime]');
render(el, 'ru');

// При уходе со страницы
window.addEventListener('unload', () => cancel(el));

React: компонент реального времени

import { useEffect, useRef } from 'react';
import { render, cancel } from 'timeago.js';

interface LiveTimeProps {
  date:    Date | string;
  locale?: string;
}

export function LiveTime({ date, locale = 'ru' }: LiveTimeProps) {
  const ref = useRef<HTMLTimeElement>(null);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;

    render(el, locale);
    return () => cancel(el);
  }, [locale]);

  const isoDate = new Date(date as any).toISOString();

  return (
    <time
      ref={ref}
      dateTime={isoDate}
    />
  );
}

Vue: компонент реального времени

<template>
  <time :datetime="isoDate" ref="timeEl" />
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { render, cancel } from 'timeago.js';

const props = defineProps<{ date: string | Date; locale?: string }>();
const timeEl = ref<HTMLTimeElement | null>(null);

const isoDate = computed(() =>
  new Date(props.date as any).toISOString()
);

onMounted(() => {
  if (timeEl.value) render(timeEl.value, props.locale ?? 'ru');
});

onUnmounted(() => {
  if (timeEl.value) cancel(timeEl.value);
});
</script>

Список с реальным временем

// React
export function PostFeed({ posts }: { posts: Post[] }) {
  const containerRef = useRef<HTMLUListElement>(null);

  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    const times = container.querySelectorAll('time[datetime]');
    render(Array.from(times), 'ru');

    return () => cancel(Array.from(times));
  }, [posts]);

  return (
    <ul ref={containerRef}>
      {posts.map(post => (
        <li key={post.id}>
          <h3>{post.title}</h3>
          <time dateTime={new Date(post.createdAt).toISOString()} />
        </li>
      ))}
    </ul>
  );
}

Паузирование при неактивной вкладке

import { render, cancel } from 'timeago.js';

const elements = document.querySelectorAll('[datetime]');

function startUpdates() { render(elements, 'ru'); }
function pauseUpdates() { cancel(elements); }

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'visible') {
    startUpdates();
  } else {
    pauseUpdates();
  }
});

startUpdates(); // При загрузке

Реальное время с WebSocket

При получении новых данных через WebSocket перезапустить render для новых элементов:

const ws = new WebSocket('wss://api.example.com/feed');

ws.onmess age = (event) => {
  const post  = JSON.parse(event.data);
  const newEl = createPostElement(post);

  document.getElementById('feed').prepend(newEl);

  // Запустить timeago для нового элемента
  const timeEl = newEl.querySelector('time');
  if (timeEl) render(timeEl, 'ru');
};

Оптимизация: обновлять только видимые элементы

import { render, cancel } from 'timeago.js';

const io = new IntersectionObserver((entries) => {
  entries.forEach(({ target, isIntersecting }) => {
    if (isIntersecting) {
      render(target, 'ru');
    } else {
      cancel(target);
    }
  });
}, { threshold: 0.1 });

document.querySelectorAll('[datetime]').forEach(el => io.observe(el));

Реальное время с fallback для SSR

function HybridTimeAgo({ date, locale = 'ru' }: { date: string; locale?: string }) {
  const [mounted, setMounted] = useState(false);
  const ref = useRef<HTMLTimeElement>(null);

  useEffect(() => {
    setMounted(true);
    if (ref.current) {
      render(ref.current, locale);
      return () => { if (ref.current) cancel(ref.current); };
    }
  }, [locale]);

  return (
    <time
      ref={ref}
      dateTime={date}
      // Fallback до монтирования (SSR)
    >
      {!mounted && new Date(date).toLocaleDateString('ru-RU')}
    </time>
  );
}