Кэширование данных

Роль кэширования в работе картографических приложений

При работе с Google Maps JavaScript API значительная часть производительности и пользовательского опыта определяется тем, насколько эффективно организовано повторное использование ранее полученных данных. Карты, маркеры, геокодирование, результаты поиска мест и географические запросы часто повторяются при перемещении по карте, изменении масштаба или возврате пользователя к уже посещённым областям.

Кэширование снижает:

  • количество сетевых запросов к API
  • задержки при отрисовке карты и объектов
  • нагрузку на клиентское приложение
  • расходы на использование платных API

Ключевой принцип заключается в том, что картографические данные обладают высокой степенью локальности: при смещении карты на небольшое расстояние большая часть информации остаётся неизменной.


Уровни кэширования в приложениях с Google Maps

Кэширование в экосистеме Google Maps JavaScript API можно разделить на несколько уровней:

1. Встроенное кэширование браузера Браузер автоматически кэширует:

  • тайлы карты (map tiles)
  • статические ресурсы API
  • некоторые служебные данные

Это кэширование контролируется HTTP-заголовками и не требует дополнительной логики со стороны разработчика.

2. Кэширование на уровне клиентского приложения Реализуется вручную:

  • localStorage
  • sessionStorage
  • IndexedDB
  • in-memory cache (объекты JavaScript)

3. Кэширование через Service Worker Используется для:

  • офлайн-доступа
  • стратегии stale-while-revalidate
  • контроля сетевых запросов

4. Серверное кэширование Применяется при проксировании запросов:

  • геокодинг
  • Places API
  • собственные API-обёртки

Кэширование геокодирования

Геокодирование (Geocoding API) — одна из самых затратных операций, так как каждый запрос преобразует текстовый адрес в координаты или наоборот.

Типичный подход кэширования:

  • ключ: нормализованный адрес
  • значение: { lat, lng, formatted_address, timestamp }
  • TTL: ограниченный период хранения

Пример реализации in-memory и localStorage кэша:

const geocodeCache = new Map();

async function geocode(address) {
  const key = address.trim().toLowerCase();

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

  const cached = localStorage.getItem(key);
  if (cached) {
    const parsed = JSON.parse(cached);
    geocodeCache.set(key, parsed);
    return parsed;
  }

  const response = await fetch(`/api/geocode?address=${encodeURIComponent(address)}`);
  const data = await response.json();

  const result = {
    lat: data.lat,
    lng: data.lng,
    formatted: data.formatted,
    timestamp: Date.now()
  };

  geocodeCache.set(key, result);
  localStorage.setItem(key, JSON.stringify(result));

  return result;
}

Пространственное кэширование (spatial caching)

При работе с картой данные часто привязаны к координатам. Эффективный подход — разбиение мира на сетку.

Используются:

  • геохеши (geohash)
  • квадродеревья (quadtree)
  • тайловая сетка (z/x/y)

Пример кэширования по геохешу:

import ngeohash from "ngeohash";

const spatialCache = new Map();

function cacheKey(lat, lng, precision = 6) {
  return ngeohash.encode(lat, lng, precision);
}

function setSpatialData(lat, lng, data) {
  const key = cacheKey(lat, lng);
  spatialCache.set(key, data);
}

function getSpatialData(lat, lng) {
  const key = cacheKey(lat, lng);
  return spatialCache.get(key);
}

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


Кэширование объектов карты (markers, overlays)

При динамической отрисовке маркеров Google Maps важно избегать повторного создания объектов при каждом изменении состояния карты.

Подходы:

  • реиспользование экземпляров маркеров
  • пул объектов (object pooling)
  • хранение состояния в структуре данных

Пример:

const markerPool = new Map();

function getMarker(map, id, position) {
  if (markerPool.has(id)) {
    return markerPool.get(id);
  }

  const marker = new google.maps.Marker({
    map,
    position
  });

  markerPool.set(id, marker);
  return marker;
}

При обновлении данных вместо пересоздания маркера выполняется обновление позиции:

function updateMarker(id, position) {
  const marker = markerPool.get(id);
  if (marker) {
    marker.setPosition(position);
  }
}

Кэширование результатов поиска Places API

Результаты поиска мест часто повторяются при панорамировании карты или изменении масштаба.

Типичная стратегия:

  • ключ: комбинация query + bounds + type
  • ограниченный TTL
  • дедупликация запросов

Пример:

const placesCache = new Map();

function buildKey(query, bounds) {
  return `${query}:${bounds.getNorthEast().toUrlValue()}_${bounds.getSouthWest().toUrlValue()}`;
}

async function searchPlaces(service, request) {
  const key = buildKey(request.query, request.bounds);

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

  return new Promise((resolve) => {
    service.textSearch(request, (results, status) => {
      if (status === "OK") {
        placesCache.set(key, results);
        resolve(results);
      } else {
        resolve([]);
      }
    });
  });
}

Ограничения и правила Google Maps Platform

При использовании Google Maps Platform необходимо учитывать ограничения на кэширование данных:

  • некоторые данные могут храниться только ограниченное время
  • запрещено долговременное хранение отдельных результатов Places API
  • требуется обновление данных при изменении условий использования сервиса
  • запрещено создание независимых баз данных на основе результатов API без соблюдения политики

Это означает, что кэширование должно быть:

  • временным
  • служебным (для ускорения UI)
  • синхронизируемым с источником данных

Кэширование тайлов карты

Тайлы карты — базовый слой визуализации. Браузеры уже используют HTTP-кэш, однако в сложных приложениях применяется дополнительная оптимизация:

  • Service Worker interception
  • Cache Storage API
  • стратегия stale-while-revalidate

Пример Service Worker:

self.addEventListener("fetch", (event) => {
  const url = new URL(event.request.url);

  if (url.hostname.includes("googleapis.com")) {
    event.respondWith(
      caches.open("maps-cache").then(async (cache) => {
        const cached = await cache.match(event.request);

        const fetchPromise = fetch(event.request).then((response) => {
          cache.put(event.request, response.clone());
          return response;
        });

        return cached || fetchPromise;
      })
    );
  }
});

In-memory кэш и стратегии инвалидирования

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

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

  • LRU (Least Recently Used)
  • TTL (time-to-live)
  • size-based eviction

Пример TTL-кэша:

class TTLCache {
  constructor(ttl = 60000) {
    this.ttl = ttl;
    this.store = new Map();
  }

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

  get(key) {
    const item = this.store.get(key);

    if (!item) return null;

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

    return item.value;
  }
}

Кэширование при перемещении карты (debounce + viewport caching)

При каждом движении карты (bounds_changed, idle) может происходить множество запросов.

Оптимизация включает:

  • debounce событий
  • кэширование по viewport
  • частичное обновление данных

Пример debounce:

function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

map.addListener(
  "bounds_changed",
  debounce(() => {
    const bounds = map.getBounds();
    fetchDataForBounds(bounds);
  }, 300)
);

IndexedDB для долговременного кэширования

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

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

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

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

const request = indexedDB.open("maps-cache", 1);

request.onupgradenee ded = () => {
  const db = request.result;
  db.createObjectStore("places", { keyPath: "id" });
};

function savePlace(db, place) {
  const tx = db.transaction("places", "readwrite");
  tx.objectStore("places").put(place);
}

Комбинированные стратегии кэширования

На практике используется несколько уровней одновременно:

  • in-memory — быстрый доступ
  • IndexedDB — долговременное хранение
  • Service Worker — управление сетевыми запросами
  • HTTP cache — тайлы и статические ресурсы

Такая архитектура обеспечивает:

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

Проблемы согласованности данных

Основные сложности:

  • устаревание геоданных
  • изменение информации о местах
  • различия между уровнями кэширования
  • необходимость синхронизации TTL между слоями

Решения:

  • централизованный слой кэш-менеджера
  • единая политика TTL
  • принудительное обновление при изменении zoom/bounds
  • версии данных в ключах кэша
function versionedKey(baseKey, version) {
  return `${baseKey}:v${version}`;
}

Оптимизация структуры ключей кэша

Ключ кэша должен отражать:

  • координаты или геохеш
  • тип запроса
  • масштаб карты
  • параметры фильтрации

Пример:

places:restaurants:6:9.123_45.678

Чем точнее ключ, тем выше точность данных, но ниже коэффициент повторного использования.


Практическая модель кэш-слоя для картографического приложения

Типовая архитектура:

  • CacheManager (единая точка доступа)
  • MemoryCache (быстрый слой)
  • PersistentCache (IndexedDB)
  • NetworkFetcher (API слой)
  • SyncPolicy (TTL и инвалидирование)

Такой слой позволяет изолировать Google Maps API от логики приложения и управлять всеми запросами централизованно.