Проблемы с производительностью

Производительность веб-карт зависит не только от скорости выполнения JavaScript-кода, но и от объёма геоданных, сложности стилей, количества отрисовываемых слоёв, особенностей работы WebGL и частоты обновления карты. В проектах с небольшими объёмами данных проблемы могут быть незаметны, однако при работе с десятками тысяч объектов, сложными векторными тайлами или постоянным обновлением информации производительность становится критически важным фактором.

Основные признаки проблем:

  • низкий FPS при перемещении карты;
  • задержки масштабирования;
  • зависания интерфейса;
  • высокая загрузка CPU;
  • повышенное потребление памяти;
  • длительная загрузка стилей и источников данных;
  • медленная реакция на действия пользователя.

Для эффективной оптимизации необходимо понимать, какие операции являются наиболее затратными.


Архитектура рендеринга и её влияние на скорость работы

MapLibre GL JS использует WebGL для аппаратного ускорения визуализации карт.

Цепочка обработки данных выглядит следующим образом:

  1. Загрузка тайлов.
  2. Разбор геометрии.
  3. Формирование буферов WebGL.
  4. Применение стилей.
  5. Отрисовка кадра.

Каждый этап требует ресурсов.

Особенно дорогостоящими являются:

  • обработка большого количества вершин;
  • создание новых буферов GPU;
  • вычисление выражений стилей;
  • перестроение слоёв после изменения данных.

Если карта вынуждена повторять эти операции слишком часто, частота кадров начинает падать.


Избыточное количество GeoJSON-данных

Наиболее распространённая причина снижения производительности — использование огромных GeoJSON-файлов.

Пример проблемного источника:

map.addSource('cities', {
    type: 'geojson',
    data: largeGeoJson
});

Файл может содержать:

  • десятки тысяч точек;
  • тысячи полигонов;
  • сложные мультиполигоны.

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

Особенно тяжёлыми являются:

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

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

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

Например, полигон может содержать:

100000 вершин

После упрощения:

5000 вершин

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

Для подготовки данных часто используются:

  • GDAL;
  • Tippecanoe;
  • Mapshaper;
  • QGIS.

Особенно эффективно упрощение для мелких масштабов, где высокая точность геометрии всё равно не видна.


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

При больших объёмах данных предпочтительнее использовать векторные тайлы.

GeoJSON загружает весь набор объектов сразу:

map.addSource('roads', {
    type: 'geojson',
    data: 'roads.geojson'
});

Векторные тайлы загружаются частями:

map.addSource('roads', {
    type: 'vector',
    tiles: [
        'https://server/{z}/{x}/{y}.pbf'
    ]
});

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

  • меньший объём трафика;
  • загрузка только видимых данных;
  • более быстрое отображение;
  • лучшая масштабируемость.

При работе с большими картографическими проектами векторные тайлы являются фактически обязательным решением.


Ограничение количества отображаемых объектов

Не все данные необходимо показывать одновременно.

Нередко встречаются ситуации:

50000 объектов
отображаются
на масштабе z=3

На таком масштабе отдельные объекты невозможно различить, однако движок всё равно тратит ресурсы на их обработку.

Оптимальный подход:

map.addLayer({
    id: 'pois',
    type: 'circle',
    source: 'pois',
    minzoom: 10
});

Слой начнёт отображаться только после достижения указанного масштаба.

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

  • снижение нагрузки на GPU;
  • уменьшение объёма вычислений;
  • ускорение рендеринга.

Кластеризация точек

Большое количество маркеров значительно снижает производительность.

Проблемный сценарий:

100000 точек
100000 подписей

Решение — кластеризация.

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

Вместо тысяч объектов отображаются агрегированные группы.

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

  • меньше элементов на экране;
  • меньше подписей;
  • более высокая скорость масштабирования;
  • лучшая читаемость карты.

Снижение количества слоёв

Каждый слой требует отдельного прохода рендеринга.

Плохо:

100 слоёв

Лучше:

10–20 слоёв

Часто множество слоёв можно объединить.

Например:

Вместо:

roads-primary
roads-secondary
roads-local
roads-service

Использовать:

roads

с фильтрами и выражениями.

Чем меньше слоёв, тем меньше переключений состояния WebGL и выше производительность.


Сложные выражения стилей

MapLibre позволяет использовать мощную систему выражений.

Пример:

[
    "case",
    ["==", ["get", "type"], "city"],
    "#ff0000",
    ["==", ["get", "type"], "town"],
    "#00ff00",
    ["==", ["get", "type"], "village"],
    "#0000ff",
    "#888888"
]

Такие конструкции вычисляются для большого количества объектов.

Особенно затратными являются:

  • вложенные case;
  • сложные фильтры;
  • многочисленные проверки свойств;
  • математические операции.

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


Минимизация фильтров

Фильтры выполняются для каждого объекта слоя.

Например:

filter: [
    "all",
    [">", ["get", "population"], 10000],
    ["==", ["get", "country"], "KZ"]
]

Если источник содержит большое количество объектов, фильтрация становится дорогой операцией.

Предпочтительно:

  • заранее подготавливать данные;
  • разделять данные на источники;
  • исключать ненужные объекты на сервере.

Частое обновление GeoJSON

Каждый вызов:

source.setData(newData);

заставляет движок:

  • заново разобрать GeoJSON;
  • перестроить тайлы;
  • пересоздать буферы.

Проблемный пример:

setInterval(() => {
    source.setData(data);
}, 100);

Обновление каждые 100 мс приводит к постоянной переработке данных.

Лучше:

setInterval(() => {
    source.setData(data);
}, 1000);

или обновлять данные только при реальном изменении.


Избыточное использование HTML-маркеров

HTML-маркеры создаются через DOM.

Пример:

new maplibregl.Marker()
    .setLngLat([71, 51])
    .addTo(map);

Несколько десятков маркеров обычно не вызывают проблем.

Однако:

5000 Marker

означают:

5000 DOM-элементов

Браузеру приходится:

  • пересчитывать стили;
  • выполнять layout;
  • выполнять repaint.

Для больших наборов данных предпочтительнее использовать слой:

type: 'circle'

или

type: 'symbol'

которые полностью отрисовываются через WebGL.


Оптимизация подписей

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

Причины:

  • поиск свободного места;
  • предотвращение пересечений;
  • расчёт якорей;
  • перестановка текста при масштабировании.

Пример слоя подписей:

{
    type: 'symbol',
    layout: {
        'text-field': ['get', 'name']
    }
}

При большом количестве объектов стоимость вычислений возрастает многократно.

Способы оптимизации:

  • отображать подписи только на крупных масштабах;
  • ограничивать набор объектов;
  • использовать кластеризацию;
  • уменьшать количество текстовых слоёв.

Работа с изображениями и иконками

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

Плохо:

1000 различных изображений

Лучше:

несколько десятков переиспользуемых иконок

Также рекомендуется:

  • использовать спрайты;
  • уменьшать размеры изображений;
  • избегать слишком крупных PNG.

Избыточные анимации

Каждая анимация требует постоянного обновления кадров.

Пример:

function animate() {
    map.triggerRepaint();
    requestAnimationFrame(animate);
}

animate();

Такой код поддерживает непрерывный рендеринг даже тогда, когда карта статична.

Последствия:

  • повышенная загрузка CPU;
  • повышенное энергопотребление;
  • нагрев устройства.

Анимации должны запускаться только при необходимости.


События и утечки производительности

Часто проблемы возникают не в самой карте, а в обработчиках событий.

Пример:

map.on('mousemove', e => {
    expensiveOperation(e);
});

Событие может вызываться десятки раз в секунду.

Решения:

  • debounce;
  • throttle;
  • кэширование результатов.

Например:

const throttledHandler =
    throttle(handleMouseMove, 100);

map.on('mousemove', throttledHandler);

Использование feature-state вместо обновления источников

Для изменения отдельных объектов нередко выполняется полное обновление GeoJSON.

Плохо:

source.setData(data);

Лучше:

map.setFeatureState(
    {
        source: 'buildings',
        id: featureId
    },
    {
        selected: true
    }
);

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

  • не происходит повторной загрузки данных;
  • не перестраиваются тайлы;
  • изменения отображаются мгновенно.

Контроль потребления памяти

Большие проекты могут сталкиваться с накоплением памяти.

Причины:

  • неиспользуемые источники;
  • скрытые слои;
  • большие GeoJSON-объекты;
  • сохранённые ссылки на данные.

Удаление ненужных сущностей:

map.removeLayer('layer-id');
map.removeSource('source-id');

Полезно регулярно освобождать ресурсы, которые больше не используются.


Профилирование производительности

Для поиска узких мест применяются инструменты браузера.

Основные инструменты:

  • Chrome DevTools Performance;
  • Chrome Memory;
  • Firefox Performance;
  • WebGL Inspector;
  • Lighthouse.

Следует анализировать:

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

Без профилирования многие проблемы оказываются скрытыми и выявляются только на слабых устройствах.


Оптимизация под мобильные устройства

Мобильные устройства обладают ограниченными ресурсами.

Наиболее эффективные меры:

  • уменьшение количества слоёв;
  • снижение детализации данных;
  • использование векторных тайлов;
  • ограничение подписей;
  • сокращение количества анимаций;
  • уменьшение числа одновременно отображаемых объектов.

Даже если карта работает быстро на настольном компьютере, это не гарантирует приемлемую производительность на смартфонах и планшетах.


Практический чек-лист оптимизации

При появлении проблем с производительностью рекомендуется последовательно проверить следующие пункты:

  1. Используются ли векторные тайлы вместо крупных GeoJSON.
  2. Выполнено ли упрощение геометрии.
  3. Ограничено ли отображение объектов по масштабу.
  4. Включена ли кластеризация точек.
  5. Нет ли чрезмерного количества слоёв.
  6. Не используются ли тысячи HTML-маркеров.
  7. Оптимизированы ли подписи.
  8. Исключены ли сложные выражения и фильтры.
  9. Не вызывается ли setData() слишком часто.
  10. Удаляются ли неиспользуемые источники и слои.
  11. Выполнено ли профилирование через DevTools.
  12. Проверена ли работа карты на мобильных устройствах.

Комплексное применение этих методов позволяет сохранять высокую частоту кадров даже при работе с крупными наборами пространственных данных и сложными интерактивными картографическими интерфейсами на базе MapLibre GL JS.