Кеширование HTTP запросов

Кеширование HTTP-запросов — ключевой механизм оптимизации клиентских приложений. Оно снижает количество сетевых обращений, ускоряет отображение данных, уменьшает нагрузку на сервер и повышает устойчивость приложения к временным проблемам сети. В Angular кеширование реализуется на уровне сервисов и перехватчиков, что позволяет централизованно управлять логикой хранения и повторного использования ответов.

Angular не предоставляет встроенного автоматического кеширования для HttpClient, что является осознанным архитектурным решением. Это даёт разработчику полный контроль над стратегией кеширования и её соответствием бизнес-требованиям.


Архитектура HTTP в Angular

Работа с HTTP в Angular строится вокруг следующих компонентов:

  • HttpClient — основной API для выполнения HTTP-запросов.
  • HttpRequest — неизменяемый объект запроса.
  • HttpResponse — объект ответа сервера.
  • HttpInterceptor — механизм перехвата и модификации запросов и ответов.

Кеширование чаще всего реализуется через HttpInterceptor, так как он позволяет:

  • анализировать каждый исходящий запрос;
  • принимать решение о возврате данных из кеша;
  • сохранять ответы для повторного использования.

Типы кеширования запросов

Кеширование на уровне сервиса

Простейший подход — хранение данных в сервисе:

@Injectable({ providedIn: 'root' })
export class ProductsService {
  private cache: Product[] | null = null;

  constructor(private http: HttpClient) {}

  getProducts(): Observable<Product[]> {
    if (this.cache) {
      return of(this.cache);
    }

    return this.http.get<Product[]>('/api/products').pipe(
      tap(data => this.cache = data)
    );
  }
}

Особенности:

  • подходит для локального кеширования конкретных данных;
  • не масштабируется при большом количестве запросов;
  • отсутствует автоматическая инвалидция.

Кеширование через HttpInterceptor

Интерцепторы позволяют реализовать универсальный кеш для всех HTTP-запросов.

Базовая структура кеша

Обычно используется Map, где ключ — URL запроса, а значение — HttpResponse.

@Injectable()
export class HttpCache {
  private cache = new Map<string, HttpResponse<any>>();

  get(url: string): HttpResponse<any> | undefined {
    return this.cache.get(url);
  }

  put(url: string, response: HttpResponse<any>): void {
    this.cache.set(url, response);
  }
}

Реализация HTTP Cache Interceptor

@Injectable()
export class CacheInterceptor implements HttpInterceptor {

  constructor(private cache: HttpCache) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    if (req.method !== 'GET') {
      return next.handle(req);
    }

    const cachedResponse = this.cache.get(req.urlWithParams);
    if (cachedResponse) {
      return of(cachedResponse.clone());
    }

    return next.handle(req).pipe(
      tap(event => {
        if (event instanceof HttpResponse) {
          this.cache.put(req.urlWithParams, event);
        }
      })
    );
  }
}

Ключевые моменты:

  • кешируются только GET-запросы;
  • используется urlWithParams для уникальности;
  • HttpResponse клонируется из-за неизменяемости.

Подключение интерцептора

@NgModule({
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: CacheInterceptor, multi: true }
  ]
})
export class AppModule {}

Angular применяет интерцепторы в порядке их регистрации.


Управление сроком жизни кеша

Без ограничения времени кеш может привести к использованию устаревших данных. Распространённый подход — добавление времени жизни (TTL).

interface CacheEntry {
  response: HttpResponse<any>;
  timestamp: number;
}
const MAX_AGE = 30000;

get(url: string): HttpResponse<any> | null {
  const entry = this.cache.get(url);
  if (!entry) return null;

  if (Date.now() - entry.timestamp > MAX_AGE) {
    this.cache.delete(url);
    return null;
  }

  return entry.response;
}

Такой механизм позволяет автоматически очищать устаревшие данные.


Инвалидация кеша

Кеш необходимо сбрасывать при:

  • POST, PUT, DELETE запросах;
  • изменении состояния приложения;
  • выходе пользователя из системы.

Простейшая стратегия — очистка кеша при любом небезопасном запросе:

if (req.method !== 'GET') {
  this.cache.clear();
}

Для более точного контроля можно удалять только связанные URL.


Кеширование с учетом заголовков

HTTP-ответы могут содержать директивы кеширования:

  • Cache-Control
  • Expires
  • ETag

Angular не обрабатывает их автоматически, но интерцептор может анализировать заголовки:

const cacheControl = event.headers.get('Cache-Control');
if (cacheControl?.includes('no-store')) {
  return;
}

Использование ETag и условных запросов

ETag позволяет серверу сообщить версию ресурса:

  1. Клиент сохраняет ETag.
  2. При следующем запросе отправляет If-None-Match.
  3. Сервер отвечает 304 Not Modified.
const etag = cachedResponse.headers.get('ETag');

const conditionalReq = req.clone({
  headers: req.headers.set('If-None-Match', etag!)
});

Это снижает объём передаваемых данных и ускоряет ответы.


Кеширование с учетом параметров и тела запроса

Для сложных запросов одного URL недостаточно. Ключ кеша может включать:

  • параметры запроса;
  • заголовки;
  • сериализованное тело.
const cacheKey = `${req.urlWithParams}|${JSON.stringify(req.body)}`;

Локальное хранилище и persistent cache

Вместо памяти можно использовать:

  • localStorage
  • sessionStorage
  • IndexedDB

Пример сохранения ответа:

localStorage.setItem(key, JSON.stringify(response.body));

Ограничения:

  • только сериализуемые данные;
  • нет поддержки HttpResponse напрямую;
  • требуется ручное восстановление структуры.

Комбинированные стратегии

На практике применяются гибридные подходы:

  • краткоживущий кеш в памяти;
  • долгоживущий кеш в IndexedDB;
  • условные запросы с ETag;
  • принудительная инвалидация по событиям.

Такой подход позволяет добиться баланса между актуальностью данных и производительностью.


Потенциальные проблемы

  • Устаревшие данные — результат неправильной инвалидции.
  • Рост памяти — отсутствие ограничения размера кеша.
  • Несоответствие бизнес-логике — универсальный кеш не учитывает контекст пользователя.
  • Сложность отладки — данные не всегда приходят с сервера.

Практические рекомендации

  • Кешировать только идемпотентные запросы.
  • Всегда учитывать срок жизни данных.
  • Явно документировать стратегию кеширования.
  • Изолировать кеш в отдельном сервисе.
  • Не смешивать бизнес-логику и инфраструктурный код.

Кеширование HTTP-запросов в Angular — это не вспомогательная оптимизация, а полноценная часть архитектуры клиентского приложения, требующая продуманного и контролируемого подхода.