Работа с Google Maps JavaScript API часто сопровождается множественными сетевыми запросами: геокодирование адресов, поиск мест, получение деталей объектов, маршрутизация. Без оптимизации такие запросы быстро приводят к избыточной нагрузке на сеть, росту задержек и увеличению расходов на API.
Ключевые проблемы при отсутствии кэширования:
Кэширование решает эти проблемы за счёт повторного использования ранее полученных данных вместо обращения к серверу.
В контексте Google Maps JavaScript API наиболее часто кэшируются следующие категории данных:
Геокодирование (Geocoding API) Результаты преобразования адреса в координаты и обратно. Это один из самых дорогих и повторяющихся типов запросов.
Поиск мест (Places API) Результаты autocomplete, nearby search, text search.
Детали места (Place Details) Расширенная информация о конкретных объектах: рейтинг, адрес, телефон, координаты.
Маршруты (Directions API) Построенные маршруты между точками, включая альтернативные варианты.
Пользовательские вычисления Результаты, полученные на основе API (например, кластеризация маркеров, фильтрация точек).
На уровне браузера чаще всего применяется многоуровневая схема:
Каждый уровень имеет свои ограничения по скорости и объёму.
Самый быстрый способ хранения результатов — объект в памяти приложения.
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)}`;
}
Геокодирование — один из самых типичных кандидатов на кэширование.
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);
}
});
});
}
Autocomplete генерирует большое количество однотипных запросов при вводе текста.
Ключевые оптимизации:
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 подходит для временного хранения данных между обновлениями страницы.
function saveToSessionCache(key, data) {
sessionStorage.setItem(key, JSON.stringify(data));
}
function loadFromSessionCache(key) {
const cached = sessionStorage.getItem(key);
return cached ? JSON.parse(cached) : null;
}
Преимущества:
Недостатки:
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:
const CACHE_TTL = 1000 * 60 * 60; // 1 час
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 });
};
}
Маршруты являются тяжёлыми объектами и часто повторяются для одинаковых точек.
Ключ кэша должен учитывать:
function buildRouteKey(origin, destination, mode) {
return `${normalizeLatLng(origin.lat, origin.lng)}|${normalizeLatLng(destination.lat, destination.lng)}|${mode}`;
}
Кэширование в контексте Google Maps API должно учитывать следующие ограничения:
Кэширование эффективно только в связке с контролем запросов:
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
В крупных приложениях кэш выделяется в отдельный слой:
Пример структуры:
/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}`;
}
Такой подход позволяет:
Кэшированные данные должны проверяться на корректность:
Добавляется версия кэша:
const CACHE_VERSION = "v1";
const key = `${CACHE_VERSION}:${address}`;