Почему ваше приложение тормозит (и как это исправить)
Вы когда-нибудь ждали, пока интерфейс перерисуется после клика? Или наблюдали, как сервер отвечает на один и тот же запрос несколько раз подряд? Знакомо? Тогда эта статья для вас. Кэширование данных — это не просто «хорошая практика», а базовый навык любого TypeScript-разработчика, который хочет, чтобы его приложение летало, а не ползло.
Сегодня мы разберём, как правильно кэшировать данные в TypeScript: от простого in-memory кэша на фронтенде до продвинутых стратегий с TTL и Redis на бэкенде. Вы получите готовые куски кода, которые можно вставить в свой проект уже сегодня.
Что такое кэширование и зачем оно нужно в TypeScript?
Кэширование — это техника временного хранения часто используемых данных в быстром хранилище. Вместо того чтобы каждый раз ходить за данными в базу или на удалённый сервер, мы сохраняем результат первого запроса и отдаём его при повторных обращениях.
Плюсы очевидны:
- Снижение времени ответа (latency) — пользователь получает данные за миллисекунды
- Уменьшение нагрузки на сервер и базу данных
- Экономия трафика и ресурсов
- Возможность работать офлайн (для фронтенда)
В TypeScript, благодаря системе типов, мы можем написать безопасное и предсказуемое кэширование, которое не сломается в рантайме. Давайте посмотрим, как это сделать.
Стратегии кэширования: выбираем правильный подход
Прежде чем писать код, нужно понять, какую стратегию вы будете использовать. От этого зависит, насколько эффективным будет ваше решение.
1. In-Memory Cache (кэш в памяти)
Самый простой и быстрый вариант. Данные хранятся в переменной, Map или объекте. Идеально для фронтенда и небольших Node.js приложений.
Минус: данные теряются при перезагрузке страницы или перезапуске сервера.
2. LocalStorage / SessionStorage (кэш в браузере)
Подходит для фронтенда, когда нужно сохранить данные между сессиями. Идеально для настроек пользователя, токенов и кэширования API-ответов.
Минус: синхронный доступ, ограничение по размеру (5-10MB).
3. Redis (кэш на сервере)
Промышленный стандарт для бэкенда. Быстрый, надёжный, с поддержкой TTL и сложных структур данных. Отлично работает с Node.js через библиотеку ioredis.
Минус: требует отдельного сервиса (Redis сервер).
4. Service Worker + Cache API (PWA)
Продвинутый кэш для Progressive Web Apps. Позволяет перехватывать сетевые запросы и отдавать кэшированные ответы. Идеально для офлайн-режима.
Пишем кэш на TypeScript: от простого к сложному
Теперь переходим к практике. Я покажу несколько реализаций, от базовой до продвинутой, с использованием TypeScript дженериков.
Простой In-Memory Cache с TTL
Начнём с классики — кэш в памяти с автоматическим удалением устаревших записей.
// cache.ts
interface CacheEntry<T> {
data: T;
expiry: number; // timestamp в миллисекундах
}
class SimpleCache<T> {
private store = new Map<string, CacheEntry<T>>();
constructor(private defaultTTL: number = 60000) {} // 60 секунд по умолчанию
set(key: string, data: T, ttl?: number): void {
const expiry = Date.now() + (ttl ?? this.defaultTTL);
this.store.set(key, { data, expiry });
}
get(key: string): T | undefined {
const entry = this.store.get(key);
if (!entry) return undefined;
if (Date.now() > entry.expiry) {
this.store.delete(key);
return undefined; // просрочено
}
return entry.data;
}
has(key: string): boolean {
return this.get(key) !== undefined;
}
clear(): void {
this.store.clear();
}
}
// Использование
const userCache = new SimpleCache<User>(300000); // 5 минут
userCache.set('user:1', { id: 1, name: 'Alice' });
const user = userCache.get('user:1'); // { id: 1, name: 'Alice' }
Этот код уже решает 80% задач по кэшированию. Обратите внимание на дженерик <T> — он гарантирует, что мы не перепутаем типы данных.
Кэширование API-запросов с дедупликацией
Часто бывает, что один и тот же запрос вызывается несколько раз одновременно. Чтобы не слать N запросов на сервер, используем дедупликацию.
// apiCache.ts
type Fetcher<T> = () => Promise<T>;
class ApiCache<T> {
private cache = new Map<string, { promise: Promise<T>; expiry: number }>();
constructor(private ttl: number = 300000) {}
async fetch(key: string, fetcher: Fetcher<T>): Promise<T> {
const existing = this.cache.get(key);
if (existing && Date.now() < existing.expiry) {
return existing.promise; // возвращаем тот же промис
}
const promise = fetcher().finally(() => {
// удаляем из кэша после завершения (если ошибка)
setTimeout(() => this.cache.delete(key), 100);
});
this.cache.set(key, { promise, expiry: Date.now() + this.ttl });
return promise;
}
}
// Использование
const apiCache = new ApiCache<Post[]>();
const posts = await apiCache.fetch('/api/posts', () => fetch('/api/posts').then(r => r.json()));
Этот подход хорош тем, что если 10 компонентов одновременно запросят одни и те же данные, будет выполнен только один запрос. Остальные получат результат из того же промиса.
Кэш в localStorage с TypeScript
Для фронтенда, где данные нужно сохранить между перезагрузками, используем localStorage. Но есть нюанс: сериализация/десериализация и обработка ошибок.
// localStorageCache.ts
interface StorageEntry<T> {
data: T;
expiry: number;
}
class LocalStorageCache<T> {
constructor(private prefix: string = 'cache:', private ttl: number = 3600000) {} // 1 час
set(key: string, data: T, ttl?: number): void {
const entry: StorageEntry<T> = {
data,
expiry: Date.now() + (ttl ?? this.ttl)
};
try {
localStorage.setItem(this.prefix + key, JSON.stringify(entry));
} catch (e) {
console.warn('LocalStorage is full, clearing old cache');
this.clearExpired();
}
}
get(key: string): T | undefined {
const raw = localStorage.getItem(this.prefix + key);
if (!raw) return undefined;
try