Тесты производительности проверяют, что использование timeago.js не создаёт узких мест: не накапливает таймеры, не замедляет начальный рендер, не вызывает избыточных обновлений DOM.
| Метрика | Что означает |
|---|---|
| Время выполнения format | Насколько быстро форматируется одна дата |
| Скорость пакетного обновления | Сколько элементов/секунду можно обновить |
| Количество таймеров | Не превышает ли число видимых элементов |
| Потребление памяти | Нет ли утечек при монтировании/размонтировании |
| Reflow-операции | Сколько пересчётов макета вызывает обновление |
import { format } from 'timeago.js';
function benchFormat(iterations = 10_000): number {
const dates = Array.from({ length: iterations }, (_, i) =>
new Date(Date.now() - i * 1000)
);
const start = performance.now();
dates.forEach(d => format(d, 'ru'));
return performance.now() - start;
}
it('format выполняет 10 000 вызовов за < 500 мс', () => {
const elapsed = benchFormat(10_000);
expect(elapsed).toBeLessThan(500);
});
it('один вызов format занимает < 0.05 мс', () => {
const iterations = 10_000;
const dates = Array.from({ length: iterations }, (_, i) =>
new Date(Date.now() - i * 1000)
);
const start = performance.now();
dates.forEach(d => format(d, 'ru'));
const elapsed = performance.now() - start;
const perCall = elapsed / iterations;
console.log(`Среднее: ${perCall.toFixed(4)} мс/вызов`);
expect(perCall).toBeLessThan(0.05);
});
it('render не накапливает таймеры при повторном вызове без cancel', () => {
// Получить начальный счётчик таймеров
const before = process._getActiveHandles().filter(h => h.constructor.name === 'Timeout').length;
const els = Array.from({ length: 10 }, () => {
const el = document.createElement('time');
el.setAttribute('datetime', new Date(Date.now() - 60_000).toISOString());
document.body.appendChild(el);
return el;
});
render(els, 'ru');
const after = process._getActiveHandles().filter(h => h.constructor.name === 'Timeout').length;
// Количество таймеров не должно расти бесконечно
expect(after - before).toBeLessThanOrEqual(els.length);
cancel(els);
els.forEach(el => document.body.removeChild(el));
});
import { format } from 'timeago.js';
it('batch update не вызывает reflow при неизменившихся значениях', () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-06-01T12:00:00Z').getTime());
const elements = Array.from({ length: 100 }, (_, i) => {
const el = document.createElement('time');
el.setAttribute('datetime', new Date(Date.now() - (i + 1) * 60_000).toISOString());
el.textContent = format(el.getAttribute('datetime')!, 'ru');
document.body.appendChild(el);
return el;
});
let writeCount = 0;
// Второй проход — значения не изменились
elements.forEach(el => {
const next = format(el.getAttribute('datetime')!, 'ru');
if (el.textContent !== next) {
el.textContent = next;
writeCount++;
}
});
expect(writeCount).toBe(0); // Ни одной лишней записи в DOM
elements.forEach(el => document.body.removeChild(el));
jest.useRealTimers();
});
import { enrichWithTimeAgo } from '../src/utils/enrich';
it('enrichWithTimeAgo обрабатывает 1000 элементов за < 100 мс', () => {
jest.useFakeTimers();
jest.setSystemTime(Date.now());
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i,
createdAt: new Date(Date.now() - i * 3600_000).toISOString(),
}));
const start = performance.now();
const result = enrichWithTimeAgo(items);
const elapsed = performance.now() - start;
expect(result).toHaveLength(1000);
expect(elapsed).toBeLessThan(100);
jest.useRealTimers();
});
it('render для 100 элементов инициализируется за < 50 мс', () => {
const els = Array.from({ length: 100 }, (_, i) => {
const el = document.createElement('time');
el.setAttribute('datetime', new Date(Date.now() - i * 60_000).toISOString());
document.body.appendChild(el);
return el;
});
const start = performance.now();
render(els, 'ru');
const elapsed = performance.now() - start;
expect(elapsed).toBeLessThan(50);
cancel(els);
els.forEach(el => document.body.removeChild(el));
});
it('отменяет все таймеры после cancel', () => {
let timerCount = 0;
const originalSetTimeout = global.setTimeout;
global.setTimeout = function (...args) {
timerCount++;
return originalSetTimeout(...args);
} as typeof setTimeout;
const el = document.createElement('time');
el.setAttribute('datetime', new Date(Date.now() - 60_000).toISOString());
document.body.appendChild(el);
render(el, 'ru');
const beforeCancel = timerCount;
cancel(el);
// После cancel таймеры перестают создаваться
global.setTimeout = originalSetTimeout;
document.body.removeChild(el);
expect(beforeCancel).toBeGreaterThan(0); // Хотя бы один таймер был создан
});
it('format укладывается в performance mark', () => {
const iterations = 1000;
const dates = Array.from({ length: iterations }, (_, i) =>
new Date(Date.now() - i * 60_000)
);
performance.mark('format-start');
dates.forEach(d => format(d, 'ru'));
performance.mark('format-end');
performance.measure('format-total', 'format-start', 'format-end');
const measures = performance.getEntriesByName('format-total');
expect(measures[0].duration).toBeLessThan(200);
performance.clearMarks();
performance.clearMeasures();
});
const PERF_THRESHOLDS = {
formatSingle: 0.05, // мс на один вызов
formatBatch1000: 100, // мс на 1000 вызовов
renderInit100: 50, // мс на render 100 элементов
};
// Используются в тестах как бюджеты производительности
// При превышении — тест падает, CI уведомляет команду