Ленивая загрузка данных в картографических приложениях опирается на идею получения только той информации, которая необходима для текущего состояния карты. Это критически важно для производительности при работе с большими наборами пространственных данных, особенно когда отображаются десятки или сотни тысяч объектов.
Ключевые цели ленивой загрузки:
MapLibre GL JS реализует ленивую загрузку через систему источников (sources), тайлов и событий изменения состояния карты.
В основе лежит tile-based подход. Данные разбиваются на векторные или растровые тайлы, которые загружаются по мере необходимости.
Типичный источник данных:
map.addSource('points', {
type: 'vector',
tiles: [
'https://example.com/tiles/{z}/{x}/{y}.pbf'
],
minzoom: 0,
maxzoom: 14
});
Здесь ключевую роль играет шаблон URL. MapLibre автоматически подставляет координаты тайла в зависимости от текущего viewport.
Параметры управления загрузкой:
minzoom — минимальный уровень масштабирования, при
котором источник активен;maxzoom — максимальный уровень детализации;{z}/{x}/{y} — координаты тайловой сетки.Часто источники не добавляются сразу, а подгружаются после первичного рендера карты.
map.on('load', () => {
map.addSource('buildings', {
type: 'geojson',
data: 'https://example.com/data/buildings.json'
});
map.addLayer({
id: 'buildings-layer',
type: 'fill',
source: 'buildings',
paint: {
'fill-color': '#3b82f6',
'fill-opacity': 0.6
}
});
});
Однако даже такой подход не всегда оптимален, если объём данных большой. В этом случае применяется частичная загрузка по области видимости.
Один из наиболее эффективных способов ленивой загрузки — подгрузка данных по текущим границам карты.
function loadDataByBounds() {
const bounds = map.getBounds();
const url = new URL('https://example.com/api/objects');
url.searchParams.set('bbox', [
bounds.getWest(),
bounds.getSouth(),
bounds.getEast(),
bounds.getNorth()
].join(','));
fetch(url)
.then(res => res.json())
.then(data => {
map.getSource('dynamic-data').setData(data);
});
}
map.on('moveend', loadDataByBounds);
Здесь ключевую роль играет событие moveend, которое
срабатывает после завершения перемещения карты, предотвращая избыточные
запросы во время панорамирования.
Без ограничения частоты запросов ленивой загрузки легко перегрузить сервер. Поэтому используется debounce.
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const loadData = debounce(() => {
const b = map.getBounds();
fetch(`/api/data?bbox=${b.toArray().flat().join(',')}`)
.then(r => r.json())
.then(data => {
map.getSource('points').setData(data);
});
}, 300);
map.on('move', loadData);
Такой подход снижает количество запросов при плавном перемещении карты.
GeoJSON становится неэффективным при больших данных. Векторные тайлы позволяют загружать только нужные фрагменты данных.
map.addSource('shops', {
type: 'vector',
tiles: [
'https://tiles.example.com/shops/{z}/{x}/{y}.pbf'
],
promoteId: 'id'
});
Преимущества:
Ленивая загрузка часто комбинируется с фильтрацией по масштабу.
map.addLayer({
id: 'cities',
type: 'circle',
source: 'cities',
minzoom: 5,
maxzoom: 10,
paint: {
'circle-radius': 6,
'circle-color': '#ef4444'
}
});
Это позволяет не загружать или не отображать данные вне нужного диапазона масштабов.
В сложных приложениях слои добавляются по мере необходимости, например при включении фильтра или открытии панели.
function enableTrafficLayer() {
if (map.getSource('traffic')) return;
map.addSource('traffic', {
type: 'vector',
tiles: [
'https://example.com/traffic/{z}/{x}/{y}.pbf'
]
});
map.addLayer({
id: 'traffic-lines',
type: 'line',
source: 'traffic',
'source-layer': 'roads',
paint: {
'line-color': '#f97316',
'line-width': 2
}
});
}
Такой подход снижает начальную нагрузку на клиент и ускоряет первичную отрисовку карты.
MapLibre предоставляет набор событий, позволяющих точно контролировать момент загрузки и обновления данных:
load — карта и стили загружены;idle — нет активных операций рендеринга;render — происходит перерисовка;data — обновление источников данных.Пример загрузки данных после полной стабилизации карты:
map.on('idle', () => {
if (!map.getSource('pois')) {
map.addSource('pois', {
type: 'geojson',
data: '/api/pois'
});
}
});
Ленивая загрузка эффективна только при наличии кэширования. Обычно применяются несколько уровней:
Пример простого кэша:
const cache = new Map();
async function fetchData(bbox) {
const key = bbox.join(',');
if (cache.has(key)) {
return cache.get(key);
}
const res = await fetch(`/api/data?bbox=${key}`);
const json = await res.json();
cache.set(key, json);
return json;
}
При больших объёмах точек часто используется серверная агрегация:
MapLibre поддерживает клиентскую кластеризацию:
map.addSource('earthquakes', {
type: 'geojson',
data: '/data/earthquakes.json',
cluster: true,
clusterMaxZoom: 12,
clusterRadius: 50
});
Это снижает количество отображаемых объектов и ускоряет рендеринг при высоких масштабах.
При быстром перемещении карты важно предотвращать гонки запросов.
let controller;
async function load() {
if (controller) controller.abort();
controller = new AbortController();
const bounds = map.getBounds();
const res = await fetch(
`/api/data?bbox=${bounds.toArray().flat().join(',')}`,
{ signal: controller.signal }
);
const data = await res.json();
map.getSource('data').setData(data);
}
map.on('moveend', load);
Это гарантирует, что отображаются только актуальные данные.
MapLibre позволяет динамически фильтровать уже загруженные данные без повторных запросов:
map.setFilter('points-layer', [
'all',
['>=', ['get', 'population'], 10000]
]);
Это снижает необходимость дополнительной загрузки данных при изменении пользовательских условий.
Ленивая загрузка в MapLibre GL JS строится на сочетании нескольких механизмов: