Оптимизация перерисовки

При большом количестве элементов с датами неоптимальное обновление может создавать заметные задержки интерфейса. Каждое обновление textContent вызывает перерисовку браузера, а многочисленные таймеры создают нагрузку на поток.


Проблемы перерисовки

  • Тhrashing layout: обновления нескольких элементов в разных местах страницы вызывают множество перерисовок.
  • Лишние обновления: элементы, не видимые пользователю, обновляются без необходимости.
  • Слишком частые таймеры: при использовании format в setInterval с малым интервалом.

Проверка перед записью

Не записывать, если значение не изменилось:

import { format } from 'timeago.js';

function updateIfChanged(element, date, locale = 'ru') {
  const newText = format(date, locale);
  if (element.textContent !== newText) {
    element.textContent = newText;
  }
}

Это предотвращает ложные перерисовки.


Батчинг обновлений через requestAnimationFrame

Объединять обновления в один кадр:

import { format } from 'timeago.js';

function batchUpdate(elements, locale = 'ru') {
  requestAnimationFrame(() => {
    elements.forEach(el => {
      const date    = el.getAttribute('datetime');
      const newText = format(date, locale);
      if (el.textContent !== newText) {
        el.textContent = newText;
      }
    });
  });
}

Использование DocumentFragment

При вставке новых элементов с датами:

import { format } from 'timeago.js';

function addPosts(posts, container) {
  const fragment = document.createDocumentFragment();

  posts.forEach(post => {
    const li = document.createElement('li');
    li.innerHTML = `
      <h3>${post.title}</h3>
      <time datetime="${post.createdAt}">
        ${format(post.createdAt, 'ru')}
      </time>
    `;
    fragment.appendChild(li);
  });

  container.appendChild(fragment); // одна вставка вместо N
}

IntersectionObserver для ленивых обновлений

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

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

function lazyTimeago(nodes, locale = 'ru') {
  const io = new IntersectionObserver((entries) => {
    entries.forEach(({ target, isIntersecting }) => {
      if (isIntersecting) {
        render(target, locale);
      } else {
        cancel(target);
      }
    });
  });

  nodes.forEach(node => io.observe(node));

  return () => {
    io.disconnect();
    cancel(nodes);
  };
}

Элементы за пределами viewport не создают таймеров.


Адаптивный интервал без render

Если render не устраивает, собственный адаптивный таймер:

import { format } from 'timeago.js';

function adaptiveUpdate(elements, locale = 'ru') {
  function getInterval(diffMs) {
    if (diffMs < 60000)    return 10000;
    if (diffMs < 3600000)  return 60000;
    if (diffMs < 86400000) return 1800000;
    return 86400000;
  }

  let timer;

  function tick() {
    elements.forEach(el => {
      const date = el.getAttribute('datetime');
      if (!date) return;

      const newText = format(date, locale);
      if (el.textContent !== newText) el.textContent = newText;
    });

    // Рассчитать следующий интервал по наиболее свежей дате
    const minDiff = Math.min(...Array.from(elements).map(el =>
      Math.abs(Date.now() - new Date(el.getAttribute('datetime')).getTime())
    ));

    clearTimeout(timer);
    timer = setTimeout(tick, getInterval(minDiff));
  }

  tick();
  return () => clearTimeout(timer);
}

CSS-анимация вместо DOM-обновления

Для элемента “только что” можно использовать CSS-анимацию пульсации вместо постоянного обновления текста:

.time-fresh {
  animation: pulse 2s infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0.6; }
}
import { format } from 'timeago.js';

function renderWithClass(el, date, locale = 'ru') {
  const diff = Date.now() - new Date(date).getTime();
  el.textContent = format(date, locale);
  el.classList.toggle('time-fresh', diff < 30000);
}

Отложенная инициализация

Не инициализировать render для всех элементов сразу:

import { render } from 'timeago.js';

// Инициализировать только первые 10 элементов
const allNodes  = Array.from(document.querySelectorAll('.time'));
const firstPage = allNodes.slice(0, 10);

render(firstPage, 'ru');

// Остальные — при прокрутке
window.addEventListener('scroll', () => {
  const visible = allNodes.filter(el => isVisible(el));
  render(visible, 'ru');
}, { passive: true });

function isVisible(el) {
  const rect = el.getBoundingClientRect();
  return rect.top < window.innerHeight && rect.bottom > 0;
}

Профилирование перерисовок

Для анализа производительности:

  1. Открыть Chrome DevTools → Performance.
  2. Записать 30 секунд работы страницы с timeago.
  3. Проверить частоту Recalculate Style и Layout.
  4. Найти вызовы от таймеров timeago и оценить их влияние.

Если Layout занимает существенную долю — оптимизировать через батчинг или IntersectionObserver.