Рендеринг карты в Mapbox GL JS опирается на потоковую загрузку множества внешних ресурсов: тайлы, стили, спрайты, glyph-шрифты, GeoJSON и изображения. Каждый из этих типов данных загружается асинхронно и подвержен сетевым сбоям, ограничениям API, таймаутам и временной недоступности CDN.
Основные источники нестабильности:
Внутренний механизм загрузки Mapbox GL JS уже содержит базовую устойчивость, однако в сложных сценариях требуется построение собственной retry-стратегии поверх стандартного поведения.
Retry в Mapbox GL JS реализуется не на одном уровне, а распределяется по нескольким слоям:
Самый критичный уровень — загрузка растровых и векторных тайлов. Здесь применяются стратегии:
Источники данных (sources) могут проваливаться целиком. Например:
При отсутствии данных возможны:
На этом уровне retry проявляется как повторная установка source / repaint / trigger reload.
Наиболее распространённая модель — экспоненциальный backoff:
Добавляется jitter для предотвращения синхронных повторов у множества клиентов.
function fetchWithRetry(url, options = {}, retries = 3, delay = 500) {
return fetch(url, options).catch(async (err) => {
if (retries <= 0) throw err;
await new Promise(r => setTimeout(r, delay));
return fetchWithRetry(url, options, retries - 1, delay * 2);
});
}
В Mapbox GL JS тайлы загружаются через TileJSON и internal tile
loader. Перехват возможен через transformRequest.
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v11',
transformRequest: (url, resourceType) => {
return {
url,
headers: {
'x-retry-attempt': '1'
}
};
}
});
Однако transformRequest сам по себе не реализует retry — он лишь позволяет внедрить метаданные и альтернативные endpoints.
Mapbox GL JS предоставляет события уровня карты:
errordatasourcedatadataloadingmap.on('error', (e) => {
const error = e.error;
if (!error) return;
if (error.status === 429) {
console.warn('Rate limit reached, scheduling retry');
}
if (error.status >= 500) {
console.warn('Server error, retry possible');
}
});
Источники данных можно перезагружать через setData
(GeoJSON) или removeSource/addSource (tile sources).
async function loadGeoJsonWithRetry(map, sourceId, url, retries = 3) {
try {
const data = await fetchWithRetry(url, {}, retries);
map.getSource(sourceId).setData(await data.json());
} catch (e) {
console.error('GeoJSON load failed permanently', e);
}
}
Jitter снижает риск “thundering herd” при массовых повторных запросах.
function backoffDelay(attempt) {
const base = Math.pow(2, attempt) * 100;
const jitter = Math.random() * 100;
return base + jitter;
}
Sprite и glyph файлы загружаются один раз при старте стиля, но их сбой критичен.
const styles = [
'mapbox://styles/mapbox/streets-v11',
'mapbox://styles/mapbox/light-v10'
];
async function loadStyleWithFallback(map, index = 0) {
if (index >= styles.length) return;
map.setStyle(styles[index]);
map.once('error', () => {
loadStyleWithFallback(map, index + 1);
});
}
При активном перемещении карты запросы устаревают. Retry без отмены приводит к перегрузке.
let controller;
function loadTiles(url) {
if (controller) controller.abort();
controller = new AbortController();
return fetch(url, { signal: controller.signal })
.catch(err => {
if (err.name === 'AbortError') return;
throw err;
});
}
Без лимитов retry превращается в лавину запросов.
Используется очередь:
class RequestQueue {
constructor(limit = 4) {
this.limit = limit;
this.active = 0;
this.queue = [];
}
enqueue(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this.next();
});
}
async next() {
if (this.active >= this.limit || !this.queue.length) return;
const { task, resolve, reject } = this.queue.shift();
this.active++;
try {
const result = await task();
resolve(result);
} catch (e) {
reject(e);
}
this.active--;
this.next();
}
}
Mapbox API активно использует ограничение запросов. Стандартная стратегия:
Retry-Afterasync function handleRateLimit(response) {
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter
? parseInt(retryAfter) * 1000
: 2000;
await new Promise(r => setTimeout(r, delay));
}
Mapbox GL JS использует WebGL render loop. Retry может влиять на перерендеринг:
Стратегия:
Retry не всегда оптимален. Часто предпочтительнее:
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('tiles')) {
event.respondWith(
caches.match(event.request).then(cached => {
return cached || fetch(event.request);
})
);
}
});
Практически применимая модель включает:
При ухудшении сети система должна переходить в режим:
Это снижает нагрузку на retry-механизм и стабилизирует визуальный слой карты.