Кэширование в TypeScript: ускоряем приложение без боли

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему ваше приложение тормозит (и как это исправить)

Вы когда-нибудь ждали, пока интерфейс перерисуется после клика? Или наблюдали, как сервер отвечает на один и тот же запрос несколько раз подряд? Знакомо? Тогда эта статья для вас. Кэширование данных — это не просто «хорошая практика», а базовый навык любого 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                

Блоги

Рекомендаций по книгам