Частые обновления DOM — основной источник производительностных проблем при использовании timeago.js. Несколько стратегий позволяют снизить нагрузку без потери функциональности.
По умолчанию библиотека записывает строку при каждом срабатывании таймера. Обёртка с проверкой:
import { format } from 'timeago.js';
function smartUpdate(el, locale = 'ru') {
const date = el.getAttribute('datetime');
const current = el.textContent;
const next = format(date, locale);
if (current !== next) {
el.textContent = next;
}
}
import { format } from 'timeago.js';
function adaptiveRenderer(elements, locale = 'ru') {
function getDelay(date) {
const diff = Math.abs(Date.now() - new Date(date).getTime());
if (diff < 60000) return 10000; // < 1 мин
if (diff < 3600000) return 60000; // < 1 ч
if (diff < 86400000) return 1800000; // < 1 д
return 86400000; // > 1 д
}
function upd ate() {
let minDelay = Infinity;
elements.forEach(el => {
const date = el.getAttribute('datetime');
if (!date) return;
const next = format(date, locale);
if (el.textContent !== next) el.textContent = next;
minDelay = Math.min(minDelay, getDelay(date));
});
setTimeout(update, minDelay);
}
update();
}
Запускать таймеры только для видимых элементов:
import { render, cancel } from 'timeago.js';
function visibilityOptimizedRender(nodes, locale = 'ru') {
const observer = new IntersectionObserver((entries) => {
entries.forEach(({ target, isIntersecting }) => {
if (isIntersecting) {
render(target, locale);
} else {
cancel(target);
}
});
});
nodes.forEach(node => observer.observe(node));
return () => {
observer.disconnect();
cancel(nodes);
};
}
Группировать обновления в один кадр:
import { format } from 'timeago.js';
let scheduled = false;
const pending = new Se t();
function scheduleUpdate(el) {
pending.add(el);
if (!scheduled) {
scheduled = true;
requestAnimationFrame(() => {
pending.forEach(el => {
const date = el.getAttribute('datetime');
if (!date) return;
const next = format(date, 'ru');
if (el.textContent !== next) el.textContent = next;
});
pending.clear();
scheduled = false;
});
}
}
Прекращать обновления для дат, которые уже не изменятся в читаемом виде:
import { render, cancel, format } from 'timeago.js';
function frozenRender(el, locale = 'ru') {
const date = el.getAttribute('datetime');
const ageMs = Date.now() - new Date(date).getTime();
// Если дата старше года — не нужен таймер
if (ageMs > 365 * 86400000) {
el.textContent = format(date, locale);
return;
}
render(el, locale);
}
Перенести расчёты в фоновый поток (подходит для очень большого количества дат):
// worker.js
import { format } from 'timeago.js';
self.onmess age = ({ data }) => {
const { dates, locale } = data;
const results = dates.map(d => format(d, locale));
self.postMessage(results);
};
// main.js
const worker = new Worker('./worker.js');
worker.postMessage({ dates: allDates, locale: 'ru' });
worker.onmess age = ({ data }) => {
elements.forEach((el, i) => {
el.textContent = data[i];
});
};
Рендерить только элементы в видимой области (react-window, vue-virtual-scroller):
import { FixedSizeList } from 'react-window';
import { format } from 'timeago.js';
function Row({ index, style, data }) {
return (
<div style={style}>
<time>{format(data[index].createdAt, 'ru')}</time>
</div>
);
}
function VirtualFeed({ posts }) {
return (
<FixedSizeList
height={600}
itemCount={posts.length}
itemSize={80}
itemData={posts}
>
{Row}
</FixedSizeList>
);
}
| Стратегия | Снижение CPU | Сложность реализации |
|---|---|---|
| Проверка перед записью | 20–30% | Низкая |
| Адаптивный интервал | 30–50% | Средняя |
| IntersectionObserver | 50–80% | Средняя |
| rAF-батчинг | 20–40% | Средняя |
| Заморозка старых дат | 10–30% | Низкая |
| Виртуализация | 80–95% | Высокая |