Кеширование результатов format позволяет избежать
повторных вычислений для одних и тех же дат и локалей. Особенно полезно
при рендеринге длинных списков, где одна дата может встречаться
несколько раз.
Без кеша каждый вызов format вычисляет разницу заново.
Для статичного списка из 100 постов с разными датами — 100 вычислений
при каждом рендере. Для длинных лент с частичным совпадением дат кеш
даёт ощутимый выигрыш.
import { format } from 'timeago.js';
const cache = new Map();
function cachedFormat(date, locale = 'ru') {
const key = `${date}:${locale}`;
if (cache.has(key)) {
return cache.get(key);
}
const result = format(date, locale);
cache.set(key, result);
return result;
}
Проблема: кеш не инвалидируется. Через час format вернёт
“5 минут назад” для значения, которое уже должно быть “час назад”.
import { format } from 'timeago.js';
interface CacheEntry {
value: string;
cachedAt: number;
}
const cache = new Map<string, CacheEntry>();
function cachedFormatWithTTL(
date: Date | string | number,
locale = 'ru',
ttlMs = 30_000
): string {
const key = `${date}:${locale}`;
const entry = cache.get(key);
const now = Date.now();
if (entry && now - entry.cachedAt < ttlMs) {
return entry.value;
}
const value = format(date, locale);
cache.set(key, { value, cachedAt: now });
return value;
}
Дата 5-минутной давности меняет своё отображение каждые 5-10 секунд. Дата двухлетней давности — раз в год. Разные TTL снижают частоту обновлений кеша:
function getTTL(date: Date | string | number): number {
const ageMs = Date.now() - new Date(date as any).getTime();
if (ageMs < 60_000) return 10_000; // < 1 минуты: кеш 10 сек
if (ageMs < 3600_000) return 60_000; // < 1 часа: кеш 1 мин
if (ageMs < 86400_000) return 1800_000; // < 1 дня: кеш 30 мин
if (ageMs < 2592000_000) return 86400_000; // < 30 дней: кеш 1 день
return 7 * 86400_000; // Старше: кеш 1 неделя
}
function adaptiveCachedFormat(date: Date | string | number, locale = 'ru'): string {
const key = `${date}:${locale}`;
const entry = cache.get(key);
const now = Date.now();
const ttl = getTTL(date);
if (entry && now - entry.cachedAt < ttl) {
return entry.value;
}
const value = format(date, locale);
cache.set(key, { value, cachedAt: now });
return value;
}
class LRUCache<K, V> {
private map = new Map<K, V>();
private max: number;
constructor(max: number) {
this.max = max;
}
get(key: K): V | undefined {
if (!this.map.has(key)) return undefined;
// Переместить в конец (самый новый)
const val = this.map.get(key)!;
this.map.delete(key);
this.map.set(key, val);
return val;
}
set(key: K, value: V): void {
if (this.map.has(key)) this.map.delete(key);
else if (this.map.size >= this.max) {
// Удалить самый старый
this.map.delete(this.map.keys().next().value);
}
this.map.set(key, value);
}
}
const lruCache = new LRUCache<string, string>(500);
Кешировать результат по DOM-элементу, а не по строке ключа:
import { format } from 'timeago.js';
const elementCache = new WeakMap<HTMLTimeElement, { value: string; ts: number }>();
function formatElement(el: HTMLTimeElement, locale = 'ru'): string {
const datetime = el.getAttribute('datetime');
if (!datetime) return '';
const cached = elementCache.get(el);
const now = Date.now();
const ttl = 60_000;
if (cached && now - cached.ts < ttl) {
return cached.value;
}
const value = format(datetime, locale);
elementCache.set(el, { value, ts: now });
return value;
}
WeakMap автоматически освобождает память при удалении элемента из DOM.
const cacheByLocale = new Map<string, Map<string, string>>();
function getLocaleCache(locale: string): Map<string, string> {
if (!cacheByLocale.has(locale)) {
cacheByLocale.set(locale, new Map());
}
return cacheByLocale.get(locale)!;
}
function clearLocaleCache(locale: string): void {
cacheByLocale.delete(locale);
}
// При смене языка в приложении
function onLocaleChange(newLocale: string): void {
// Кеш для новой локали будет строиться заново
// Старый кеш можно оставить (не удалять) — он перестанет использоваться
}
function clearFormatCache(): void {
cache.clear();
console.debug('[timeago] Cache cleared');
}
// Очищать при фокусе вкладки после долгого отсутствия
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
clearFormatCache(); // Принудительно обновить все значения
}
});
let hits = 0;
let misses = 0;
function trackedCachedFormat(date, locale = 'ru') {
const key = `${date}:${locale}`;
if (cache.has(key)) {
hits++;
return cache.get(key);
}
misses++;
const value = format(date, locale);
cache.set(key, value);
return value;
}
// В DevTools
console.log(`Cache hits: ${hits}, misses: ${misses}, ratio: ${(hits / (hits + misses) * 100).toFixed(1)}%`);