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

HERE Technologies предоставляет широкий набор геосервисов, которые в JavaScript-приложениях обычно используются через HERE Maps API JavaScript. При интенсивной работе с геокодированием, маршрутизацией, тайлами и поиском объектов ключевым фактором производительности становится корректная организация кэширования запросов.

Кэширование в контексте картографических API — это не только снижение количества сетевых вызовов, но и стабилизация UX, уменьшение задержек, контроль расходов и соблюдение лимитов API.


Категории данных, подлежащих кэшированию

Геокодирование и обратное геокодирование

Геокодирование (address → coordinates) и обратное геокодирование (coordinates → address) — одни из самых дорогих и часто вызываемых операций.

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

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

Пример запроса:

platform.getSearchService().geocode({
  q: 'Almaty, Abay Street 10'
}, result => {
  console.log(result);
});

Маршрутизация

Запросы маршрутов (routing) менее повторяемы, но часто имеют локальные повторения:

  • одинаковые точки отправления/назначения
  • повторяющиеся сценарии доставки
  • пользовательские маршруты «дом–работа»

Особенность маршрутов — высокая чувствительность к параметрам:

  • тип транспорта
  • ограничения
  • дорожная обстановка

Тайлы карты

Тайлы (map tiles) — наиболее кэшируемый слой:

  • статичны в пределах zoom/viewport
  • агрессивно кэшируются браузером
  • подходят для service worker caching

Поиск POI (Points of Interest)

Поиск объектов:

  • кафе, АЗС, организации
  • автодополнение строк

Запросы часто повторяются с минимальными отличиями.


Принципы построения кэша

Нормализация ключей

Главная проблема кэширования геозапросов — вариативность входных данных.

Пример проблемы:

  • "Almaty, Abay 10"
  • "Abay street 10, Almaty"
  • " almaty abay 10"

Решение — нормализация:

  • приведение к нижнему регистру
  • удаление лишних пробелов
  • сортировка параметров
  • стандартизация формата координат
function normalizeQuery(query) {
  return query
    .trim()
    .toLowerCase()
    .replace(/\s+/g, ' ');
}

Структура ключа кэша

Для разных типов запросов ключ должен включать:

  • тип операции (geocode, route, search)
  • входные параметры
  • дополнительные опции

Пример:

function buildCacheKey(type, params) {
  return `${type}:${JSON.stringify(params)}`;
}

Более строгий вариант:

function buildCacheKey(type, params) {
  return type + ':' + btoa(unescape(encodeURIComponent(
    JSON.stringify(params)
  )));
}

TTL (Time To Live)

Геоданные имеют разную «живучесть»:

  • геокодирование: дни или недели
  • POI: часы или дни
  • маршруты: минуты или часы
  • тайлы: длительный или бессрочный кэш

Пример структуры:

const cacheEntry = {
  data: result,
  timestamp: Date.now(),
  ttl: 1000 * 60 * 60 // 1 час
};

Кэширование в памяти (in-memory)

Самый простой слой — объект Map:

class MemoryCache {
  constructor() {
    this.store = new Map();
  }

  set(key, value, ttl = 60000) {
    this.store.set(key, {
      value,
      expiry: Date.now() + ttl
    });
  }

  get(key) {
    const entry = this.store.get(key);
    if (!entry) return null;

    if (Date.now() > entry.expiry) {
      this.store.delete(key);
      return null;
    }

    return entry.value;
  }
}

Применение:

const cache = new MemoryCache();

function cachedGeocode(params, callback) {
  const key = buildCacheKey('geocode', params);
  const cached = cache.get(key);

  if (cached) {
    callback(cached);
    return;
  }

  platform.getSearchService().geocode(params, result => {
    cache.set(key, result, 3600000);
    callback(result);
  });
}

Кэширование через localStorage

Подходит для:

  • повторного использования данных между сессиями
  • небольших ответов (геокодирование, POI)

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

  • синхронный доступ
  • ограниченный объём
  • отсутствие сложной инвалидации
function setLocalCache(key, data, ttl) {
  const payload = {
    data,
    expiry: Date.now() + ttl
  };
  localStorage.setItem(key, JSON.stringify(payload));
}

function getLocalCache(key) {
  const raw = localStorage.getItem(key);
  if (!raw) return null;

  const parsed = JSON.parse(raw);

  if (Date.now() > parsed.expiry) {
    localStorage.removeItem(key);
    return null;
  }

  return parsed.data;
}

IndexedDB для геоданных

IndexedDB предпочтительнее для:

  • больших объёмов POI
  • кэша маршрутов
  • офлайн-сценариев

Структура:

  • objectStore: geocode, routes, search
  • индекс по cacheKey
  • TTL как поле записи

Псевдологика:

const record = {
  key: 'geocode:almaty_10',
  value: {...},
  expiry: Date.now() + 86400000
};

Кэширование HTTP-уровня

HERE Maps API JavaScript использует HTTP-запросы к backend-сервисам, что позволяет применять:

Cache-Control

  • public
  • max-age
  • stale-while-revalidate

ETag

Позволяет избегать повторной загрузки идентичных данных.


Service Worker как слой кэширования карт

Service Worker особенно эффективен для:

  • тайлов карты
  • статических ресурсов SDK
  • API ответов с высокой повторяемостью

Пример стратегии:

self.addEventListener('fetch', event => {
  if (event.request.url.includes('tile')) {
    event.respondWith(
      caches.open('tiles-cache').then(cache => {
        return cache.match(event.request).then(response => {
          return response || fetch(event.request).then(fetchRes => {
            cache.put(event.request, fetchRes.clone());
            return fetchRes;
          });
        });
      })
    );
  }
});

Deduplication (устранение дублирующих запросов)

Если несколько компонентов одновременно запрашивают одинаковые данные:

const pendingRequests = new Map();

function dedupedRequest(key, fn) {
  if (pendingRequests.has(key)) {
    return pendingRequests.get(key);
  }

  const promise = fn().finally(() => {
    pendingRequests.delete(key);
  });

  pendingRequests.set(key, promise);
  return promise;
}

Использование:

function geocode(params) {
  const key = buildCacheKey('geocode', params);

  return dedupedRequest(key, () =>
    platform.getSearchService().geocode(params)
  );
}

Stale-While-Revalidate

Подход, при котором:

  • сначала возвращается кэш
  • затем выполняется обновление в фоне
async function swr(key, fetchFn, cache) {
  const cached = cache.get(key);

  if (cached) {
    fetchFn().then(newData => {
      cache.set(key, newData, 3600000);
    });

    return cached;
  }

  const fresh = await fetchFn();
  cache.set(key, fresh, 3600000);
  return fresh;
}

Кэширование маршрутов и их специфика

Маршруты чувствительны к:

  • пробкам
  • времени суток
  • профилю транспорта

Поэтому кэширование требует сегментации:

function routeCacheKey(params) {
  return [
    params.origin,
    params.destination,
    params.mode,
    new Date().toISOString().slice(0, 13) // часовой слот
  ].join('|');
}

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

Основные стратегии:

По времени

  • TTL истёк → удаление

По событию

  • изменение настроек маршрута
  • смена региона

По версии данных

  • обновление API
  • изменение структуры ответа

Типичные ошибки при кэшировании гео-запросов

  • отсутствие нормализации координат
  • кэширование маршрутов без учёта времени
  • смешивание разных типов запросов в одном ключе
  • игнорирование лимитов API
  • чрезмерно долгий TTL для динамических данных

Комбинированная архитектура кэша

Практически применимая схема:

  • MemoryCache → быстрый доступ
  • IndexedDB → долговременное хранилище
  • Service Worker → сетевой слой
  • HTTP cache → инфраструктурный уровень
  • Deduplication layer → защита от дублей

Иерархия:

  1. Memory
  2. IndexedDB
  3. Network (HERE API)
  4. Persisted HTTP cache

Оптимизация под масштабные приложения

При росте нагрузки критично:

  • разделять кэш по типам запросов
  • ограничивать размер in-memory кэша
  • использовать LRU-стратегии
  • учитывать стоимость API вызова
  • внедрять батчинг запросов поиска

LRU-пример:

class LRUCache {
  constructor(limit = 100) {
    this.limit = limit;
    this.map = new Map();
  }

  get(key) {
    if (!this.map.has(key)) return null;
    const value = this.map.get(key);
    this.map.delete(key);
    this.map.set(key, value);
    return value;
  }

  set(key, value) {
    if (this.map.size >= this.limit) {
      const firstKey = this.map.keys().next().value;
      this.map.delete(firstKey);
    }
    this.map.set(key, value);
  }
}