Кэширование результатов

Причины необходимости кэширования

Работа с Google Maps JavaScript API часто сопровождается множественными сетевыми запросами: геокодирование адресов, поиск мест, получение деталей объектов, маршрутизация. Без оптимизации такие запросы быстро приводят к избыточной нагрузке на сеть, росту задержек и увеличению расходов на API.

Ключевые проблемы при отсутствии кэширования:

  • повторные запросы с идентичными параметрами;
  • высокая задержка при интерактивной работе с картой;
  • превышение квот API;
  • нестабильное поведение интерфейса при медленном соединении.

Кэширование решает эти проблемы за счёт повторного использования ранее полученных данных вместо обращения к серверу.


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

В контексте Google Maps JavaScript API наиболее часто кэшируются следующие категории данных:

Геокодирование (Geocoding API) Результаты преобразования адреса в координаты и обратно. Это один из самых дорогих и повторяющихся типов запросов.

Поиск мест (Places API) Результаты autocomplete, nearby search, text search.

Детали места (Place Details) Расширенная информация о конкретных объектах: рейтинг, адрес, телефон, координаты.

Маршруты (Directions API) Построенные маршруты между точками, включая альтернативные варианты.

Пользовательские вычисления Результаты, полученные на основе API (например, кластеризация маркеров, фильтрация точек).


Базовая стратегия кэширования на клиенте

На уровне браузера чаще всего применяется многоуровневая схема:

  1. Память (in-memory cache)
  2. localStorage / sessionStorage
  3. IndexedDB для больших объёмов данных

Каждый уровень имеет свои ограничения по скорости и объёму.


In-memory кэширование

Самый быстрый способ хранения результатов — объект в памяти приложения.

const geocodeCache = new Map();

async function geocode(address) {
  if (geocodeCache.has(address)) {
    return geocodeCache.get(address);
  }

  const result = await geocoder.geocode({ address });
  geocodeCache.set(address, result);
  return result;
}

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

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

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

Одна из критичных задач — единый формат ключа. Без нормализации кэш теряет эффективность.

Примеры нормализации:

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

Для координат:

function normalizeLatLng(lat, lng, precision = 5) {
  return `${lat.toFixed(precision)},${lng.toFixed(precision)}`;
}

Кэширование результатов Geocoding API

Геокодирование — один из самых типичных кандидатов на кэширование.

const geocodeCache = new Map();

function geocodeAddress(address) {
  const key = normalizeAddress(address);

  if (geocodeCache.has(key)) {
    return Promise.resolve(geocodeCache.get(key));
  }

  return new Promise((resolve, reject) => {
    geocoder.geocode({ address }, (results, status) => {
      if (status === "OK") {
        geocodeCache.set(key, results);
        resolve(results);
      } else {
        reject(status);
      }
    });
  });
}

Кэширование Places Autocomplete

Autocomplete генерирует большое количество однотипных запросов при вводе текста.

Ключевые оптимизации:

  • debounce ввода;
  • кэширование префиксов;
  • ограничение частоты запросов.
const autocompleteCache = new Map();

function fetchAutocomplete(query) {
  const key = query.toLowerCase();

  if (autocompleteCache.has(key)) {
    return Promise.resolve(autocompleteCache.get(key));
  }

  return new Promise((resolve) => {
    service.getPlacePredictions({ input: query }, (predictions) => {
      autocompleteCache.set(key, predictions);
      resolve(predictions);
    });
  });
}

Дедупликация запросов

Даже при наличии кэша возможны параллельные идентичные запросы. Для этого используется механизм “in-flight caching”.

const pendingRequests = new Map();

function geocodeWithDedup(address) {
  const key = normalizeAddress(address);

  if (pendingRequests.has(key)) {
    return pendingRequests.get(key);
  }

  const request = new Promise((resolve, reject) => {
    geocoder.geocode({ address }, (results, status) => {
      pendingRequests.delete(key);

      if (status === "OK") {
        resolve(results);
      } else {
        reject(status);
      }
    });
  });

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

Кэширование в sessionStorage

sessionStorage подходит для временного хранения данных между обновлениями страницы.

function saveToSessionCache(key, data) {
  sessionStorage.setItem(key, JSON.stringify(data));
}

function loadFromSessionCache(key) {
  const cached = sessionStorage.getItem(key);
  return cached ? JSON.parse(cached) : null;
}

Преимущества:

  • простота;
  • автоматическое очищение при закрытии вкладки.

Недостатки:

  • ограниченный объём;
  • синхронный API.

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

localStorage используется для долговременного хранения часто используемых данных.

Типичный сценарий — сохранение популярных геокодированных адресов или избранных мест.

function setLocalCache(key, value) {
  localStorage.setItem(key, JSON.stringify({
    value,
    timestamp: Date.now()
  }));
}

function getLocalCache(key, ttl = 86400000) {
  const item = localStorage.getItem(key);
  if (!item) return null;

  const parsed = JSON.parse(item);

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

  return parsed.value;
}

TTL и управление устареванием данных

Кэш без стратегии устаревания может привести к некорректным данным.

Основные подходы:

  • time-based expiration (TTL);
  • LRU-очистка;
  • ручная инвалидация.

Пример TTL:

const CACHE_TTL = 1000 * 60 * 60; // 1 час

IndexedDB для сложных сценариев

IndexedDB применяется при больших объёмах данных: маршруты, массивы мест, пользовательские слои.

Основные преимущества:

  • асинхронный доступ;
  • большие объёмы хранения;
  • индексирование.

Пример абстракции:

function saveRoute(id, route) {
  const request = indexedDB.open("mapsCache", 1);

  request.onsucc ess = () => {
    const db = request.result;
    const tx = db.transaction("routes", "readwrite");
    tx.objectStore("routes").put({ id, route });
  };
}

Кэширование маршрутов Directions API

Маршруты являются тяжёлыми объектами и часто повторяются для одинаковых точек.

Ключ кэша должен учитывать:

  • точку отправления;
  • точку назначения;
  • режим транспорта.
function buildRouteKey(origin, destination, mode) {
  return `${normalizeLatLng(origin.lat, origin.lng)}|${normalizeLatLng(destination.lat, destination.lng)}|${mode}`;
}

Ограничения кэширования

Кэширование в контексте Google Maps API должно учитывать следующие ограничения:

  • данные могут изменяться со временем (особенно Places);
  • условия использования API могут ограничивать хранение некоторых типов данных;
  • нельзя кэшировать данные в нарушение правил лицензирования Google;
  • устаревшие координаты или маршруты могут приводить к ошибкам интерфейса.

Оптимизация частоты запросов

Кэширование эффективно только в связке с контролем запросов:

  • debounce для текстового ввода;
  • throttle для событий карты (drag, zoom);
  • batch-запросы при возможности.
function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

Слой кэширования как архитектурный компонент

В крупных приложениях кэш выделяется в отдельный слой:

  • Cache Manager;
  • API Wrapper;
  • Storage Adapter.

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

/api
  mapsApi.js
/cache
  memoryCache.js
  storageCache.js
  cacheManager.js
/services
  geocodeService.js

Cache Manager объединяет источники:

async function getGeocode(address) {
  const cached = memoryCache.get(address);
  if (cached) return cached;

  const stored = storageCache.get(address);
  if (stored) {
    memoryCache.set(address, stored);
    return stored;
  }

  const result = await api.geocode(address);
  memoryCache.set(address, result);
  storageCache.set(address, result);

  return result;
}

Поведение кэша при интерактивной карте

При работе с картой особое внимание требуется к событиям:

  • zoom_changed;
  • dragend;
  • bounds_changed.

Без кэширования каждое изменение может инициировать новый запрос, особенно при использовании видимых областей (viewport-based queries).


Гео-кэширование по тайлам и области карты

Часто применяется разбиение карты на “сетки”:

function getTileKey(lat, lng, zoom) {
  const scale = Math.pow(2, zoom);
  const x = Math.floor((lng + 180) / 360 * scale);
  const y = Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * scale);

  return `${zoom}/${x}/${y}`;
}

Такой подход позволяет:

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

Безопасность и целостность кэша

Кэшированные данные должны проверяться на корректность:

  • валидация структуры ответа;
  • защита от повреждённых JSON;
  • контроль версий API.

Добавляется версия кэша:

const CACHE_VERSION = "v1";
const key = `${CACHE_VERSION}:${address}`;