Оптимизация больших GeoJSON файлов

Большие GeoJSON-файлы становятся узким местом в рендеринге геоданных в браузере: увеличивается время парсинга JSON, возрастает нагрузка на память, замедляется пересчёт стилей и падает FPS при панорамировании и зуме.

В контексте MapLibre GL JS ключевая проблема проявляется на нескольких уровнях:

  • загрузка и десериализация GeoJSON в JavaScript
  • преобразование геометрий в внутренние структуры источника
  • генерация и обновление тайлов на лету
  • фильтрация и отрисовка большого количества слоёв

Особенно критичны наборы данных от десятков тысяч до миллионов объектов, где даже простые операции вроде setData() приводят к блокировке main thread.


Снижение объёма данных до загрузки

Первый уровень оптимизации выполняется до попадания данных в клиент.

Упрощение геометрии

Сложные полигоны с большим числом вершин создают непропорциональную нагрузку на WebGL-пайплайн.

Типовые методы:

  • Douglas-Peucker simplification
  • удаление коллинеарных точек
  • агрегация мелких сегментов

Пример упрощения через Turf.js:

import simplify from '@turf/simplify';

const simplified = simplify(geojson, {
  tolerance: 0.01,
  highQuality: false,
});

Квантование координат

Квантование уменьшает точность координат без заметной потери визуального качества:

  • сокращение количества уникальных float-значений
  • улучшение сжимаемости JSON
  • ускорение парсинга

Разделение данных по уровням масштаба

Один из ключевых подходов — предварительное разбиение:

  • low zoom: агрегированные объекты
  • mid zoom: упрощённые геометрии
  • high zoom: полные детали

Это снижает нагрузку на отрисовку в ранних зумах, где детали не нужны.


Использование векторных тайлов вместо GeoJSON

Наиболее эффективный подход — отказ от GeoJSON в рантайме и переход к тайловой модели.

Обычно применяется генерация через tippecanoe:

tippecanoe -o data.mbtiles \
  --layer=points \
  --minimum-zoom=0 \
  --maximum-zoom=14 \
  data.geojson

Далее тайлы подключаются как источник:

map.addSource('tiles', {
  type: 'vector',
  url: 'mbtiles://data'
});

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

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

Промежуточный слой geojson-vt

Когда переход на vector tiles невозможен, используется тайлинг на клиенте.

Библиотека geojson-vt разбивает GeoJSON на виртуальные тайлы:

  • уменьшает нагрузку на WebGL
  • позволяет частично сохранить GeoJSON API
  • ускоряет фильтрацию по viewport

Пример:

import geojsonvt from 'geojson-vt';

const tileIndex = geojsonvt(geojson, {
  maxZoom: 14,
  tolerance: 3,
  extent: 4096,
});

Интеграция с MapLibre:

map.addSource('vt', {
  type: 'vector',
  tiles: ['http://localhost:8080/{z}/{x}/{y}.pbf']
});

Использование кластеризации для точечных данных

Для больших наборов точек критично включать кластеризацию.

map.addSource('points', {
  type: 'geojson',
  data: geojson,
  cluster: true,
  clusterMaxZoom: 14,
  clusterRadius: 50
});

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

  • снижает количество DOM/WebGL-объектов
  • заменяет сотни точек одним кластером
  • ускоряет перерасчёт при зуме

Дополнительные слои:

map.addLayer({
  id: 'clusters',
  type: 'circle',
  source: 'points',
  filter: ['has', 'point_count']
});

map.addLayer({
  id: 'cluster-count',
  type: 'symbol',
  source: 'points',
  filter: ['has', 'point_count']
});

Фильтрация по viewport

Один из скрытых источников нагрузки — отрисовка объектов вне области видимости.

Оптимизация:

  • предварительная фильтрация bounding box
  • динамическое обновление setFilter
  • ограничение источника по геометрии

Пример простого bbox-фильтра:

function filterByBounds(features, bounds) {
  return features.filter(f => {
    const [lng, lat] = f.geometry.coordinates;
    return bounds.contains([lng, lat]);
  });
}

Минимизация обновлений источника

Частая ошибка — постоянный вызов setData() при малейших изменениях.

Оптимизации:

  • debounce обновлений
  • батчинг изменений
  • immutable data patterns
let timeout;

function updateSource(data) {
  clearTimeout(timeout);
  timeout = setTimeout(() => {
    map.getSource('points').setData(data);
  }, 200);
}

Использование promoteId

При работе с интерактивными слоями важно стабилизировать идентификаторы:

map.addSource('geo', {
  type: 'geojson',
  data: geojson,
  promoteId: 'id'
});

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

  • ускорение feature-state
  • уменьшение внутренних lookup-операций
  • стабильная идентификация объектов при перерисовке

Оптимизация стилей слоёв

Каждый слой увеличивает стоимость композиции кадра.

Практики оптимизации:

  • минимизация количества слоёв
  • объединение фильтров
  • отказ от сложных выражений match и case
  • использование paint свойств вместо динамических вычислений

Пример менее затратной структуры:

map.addLayer({
  id: 'points',
  type: 'circle',
  source: 'points',
  paint: {
    'circle-radius': 4,
    'circle-color': '#3b82f6'
  }
});

Снижение нагрузки через zoom-based data splitting

Разделение данных по диапазонам зума снижает количество обрабатываемых объектов:

map.addSource('low', { type: 'geojson', data: lowZoomData });
map.addSource('high', { type: 'geojson', data: highZoomData });

И фильтрация слоями:

'minzoom': 0,
'maxzoom': 10

и

'minzoom': 10,
'maxzoom': 24

Асинхронная подготовка данных

Тяжёлые операции необходимо переносить из main thread.

Использование Web Workers:

// worker.js
self.onmess age = function (e) {
  const simplified = simplify(e.data);
  self.postMessage(simplified);
};

Основной поток:

worker.postMessage(geojson);
worker.onmess age = (e) => {
  map.getSource('geo').setData(e.data);
};

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

При повторной загрузке данных важно избегать повторного парсинга:

  • хранение preprocessed GeoJSON
  • использование IndexedDB
  • HTTP cache headers
  • gzip/brotli сжатие

Особенно эффективно кеширование уже упрощённых данных вместо исходных.


Ограничение плотности отрисовки

При высоких зумах количество объектов может становиться избыточным.

Подходы:

  • динамическое уменьшение opacity для перегруженных зон
  • spatial thinning (разрежение точек)
  • ограничение max features per tile

Итоговая архитектура оптимизированного пайплайна

Типовая высокопроизводительная схема обработки больших GeoJSON:

  • предварительная simplification
  • разбиение по zoom уровням
  • генерация vector tiles или geojson-vt
  • кластеризация точек
  • Web Worker preprocessing
  • минимизация setData вызовов
  • использование promoteId
  • строгая viewport фильтрация
  • оптимизированные style layers

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