Производительность анимации

Анимация является одной из ключевых возможностей интерактивных карт. Плавное перемещение камеры, анимированные маркеры, визуализация потоков данных, изменение стилей и динамическое обновление слоёв позволяют создавать современные геоинформационные интерфейсы. Однако большое количество объектов, частые перерисовки и неэффективная работа с данными способны привести к падению FPS, повышенной нагрузке на процессор и ухудшению пользовательского опыта.

Mapbox GL JS использует аппаратное ускорение через WebGL, что обеспечивает высокую производительность даже при отображении большого количества геоданных. Тем не менее эффективность анимации во многом зависит от архитектуры приложения и способов взаимодействия с API библиотеки.


Как работает цикл отрисовки

В основе работы Mapbox GL JS лежит цикл рендеринга, связанный с браузерным механизмом requestAnimationFrame.

Каждый кадр включает несколько этапов:

  1. Обновление состояния карты.
  2. Пересчёт геометрии и стилей.
  3. Передача данных в GPU.
  4. Выполнение шейдеров.
  5. Отрисовка итогового изображения.

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

Для плавной анимации желательно поддерживать:

  • 60 FPS — идеальный показатель;
  • 30 FPS — допустимый уровень;
  • менее 20 FPS — заметные рывки и задержки.

При частоте 60 кадров в секунду на подготовку одного кадра остаётся примерно 16,7 миллисекунды.


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

Любые пользовательские анимации рекомендуется строить через requestAnimationFrame.

Пример движения маркера:

let start;

function animate(timestamp) {
    if (!start) {
        start = timestamp;
    }

    const progress = timestamp - start;

    marker.setLngLat([
        37.6173 + progress * 0.00001,
        55.7558
    ]);

    requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

Преимущества такого подхода:

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

Использование setInterval() и setTimeout() для сложной анимации обычно менее эффективно.


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

Одной из самых дорогостоящих операций является обновление данных через setData().

Неэффективный вариант:

source.setData(newGeoJson);

Если такой вызов выполняется десятки раз в секунду, библиотеке приходится:

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

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


Анимация через feature-state

Для динамического изменения свойств объектов предпочтительно использовать механизм Feature State.

Создание состояния:

map.setFeatureState(
    {
        source: 'cities',
        id: 1
    },
    {
        selected: true
    }
);

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

'circle-color': [
    'case',
    ['boolean', ['feature-state', 'selected'], false],
    '#ff0000',
    '#0000ff'
]

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

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

Избежание постоянного пересоздания слоёв

Добавление и удаление слоя является тяжёлой операцией.

Плохо:

map.removeLayer('route');
map.addLayer(layerConfig);

Лучше изменять свойства существующего слоя:

map.setPaintProperty(
    'route',
    'line-opacity',
    0.5
);

или

map.setLayoutProperty(
    'route',
    'visibility',
    'none'
);

Такой подход сохраняет существующие структуры данных и уменьшает объём перерасчётов.


Эффективная анимация камеры

Mapbox GL JS предоставляет встроенные механизмы анимации камеры.

flyTo

map.flyTo({
    center: [37.6173, 55.7558],
    zoom: 12
});

easeTo

map.easeTo({
    center: [37.6173, 55.7558],
    duration: 3000
});

jumpTo

map.jumpTo({
    center: [37.6173, 55.7558]
});

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

Метод Анимация Нагрузка
jumpTo Нет Минимальная
easeTo Да Средняя
flyTo Да Наиболее высокая

Для частых обновлений положения камеры предпочтительнее использовать easeTo() или даже прямое изменение координат через jumpTo().


Ограничение частоты обновления

Не каждая задача требует обновления на каждом кадре.

Например, движение транспорта может обновляться раз в 100–200 миллисекунд.

let lastUpdate = 0;

function animate(timestamp) {
    if (timestamp - lastUpdate > 100) {
        updateVehicles();
        lastUpdate = timestamp;
    }

    requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

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


Работа с большими объёмами данных

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

Основные причины:

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

Для оптимизации рекомендуется:

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

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

Без кластеризации:

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

С кластеризацией:

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

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

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

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

Каждое выражение вычисляется для множества объектов.

Сложное выражение:

[
    'case',
    ['>', ['get', 'population'], 1000000],
    '#ff0000',
    ['>', ['get', 'population'], 500000],
    '#00ff00',
    '#0000ff'
]

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

Если значения редко меняются, полезно заранее вычислять нужные параметры на сервере или во время подготовки данных.


Анимация пользовательских слоёв

Mapbox GL JS позволяет создавать собственные WebGL-слои.

Пример структуры:

const customLayer = {
    id: 'custom',
    type: 'custom',

    onAdd(map, gl) {
        // инициализация
    },

    render(gl, matrix) {
        // отрисовка
    }
};

Метод render() вызывается на каждом кадре.

Поэтому внутри него необходимо избегать:

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

Лучшей практикой является предварительная подготовка ресурсов в onAdd().


Повторное использование объектов

Создание объектов внутри цикла анимации приводит к постоянной работе сборщика мусора.

Неэффективно:

function animate() {
    const point = {
        lng: 37.6,
        lat: 55.7
    };
}

Лучше:

const point = {
    lng: 37.6,
    lat: 55.7
};

function animate() {
    point.lng += 0.001;
}

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


Контроль масштабирования и детализации

Чем выше уровень масштабирования, тем больше данных приходится отображать.

Для ограничения нагрузки можно использовать:

minzoom

и

maxzoom

Например:

map.addLayer({
    id: 'buildings',
    type: 'fill-extrusion',
    source: 'buildings',
    minzoom: 15
});

До достижения указанного масштаба слой не участвует в рендеринге.


Использование событий карты вместо постоянного опроса

Неэффективно:

setInterval(() => {
    updateData();
}, 50);

Эффективнее:

map.on('moveend', () => {
    updateData();
});

Также полезны события:

zoom
zoomend
move
moveend
rotate
pitch
render
idle

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


Оптимизация анимированных маркеров

HTML-маркеры создаются поверх WebGL-карты.

Пример:

new mapboxgl.Marker(element)

Несколько десятков таких объектов обычно не создают проблем, однако сотни и тысячи маркеров начинают существенно нагружать DOM.

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

  • Circle Layer;
  • Symbol Layer;
  • пользовательские WebGL-слои.

Они полностью обрабатываются графическим процессором.


Использование Canvas для сложной анимации

Если необходимо отображать большое количество движущихся объектов, иногда эффективнее рисовать их на Canvas.

Источник Canvas:

map.addSource('canvas-source', {
    type: 'canvas',
    canvas: 'canvas-id',
    coordinates: [...]
});

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

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

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

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

Chrome DevTools Performance

Позволяет определить:

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

FPS Meter

Во время тестирования полезно отслеживать:

  • стабильность FPS;
  • пики нагрузки;
  • просадки во время масштабирования карты.

Memory Profiler

Помогает обнаруживать:

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

Типичные причины падения FPS

Наиболее распространённые проблемы:

Частый вызов setData

source.setData(...)

Массовое использование HTML-маркеров

new mapboxgl.Marker(...)

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

['case', ...]

Пересоздание слоёв

removeLayer()
addLayer()

Создание объектов в каждом кадре

const obj = {};

Избыточные вычисления внутри render

render(gl, matrix) {
    expensiveCalculation();
}

Каждая из перечисленных ошибок может быть практически незаметной на небольших проектах, но при работе с крупными наборами пространственных данных способна привести к серьёзному ухудшению производительности анимации.


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

Для небольших наборов данных:

  • использовать встроенные анимации камеры;
  • применять requestAnimationFrame;
  • обновлять только изменившиеся объекты.

Для средних проектов:

  • активно использовать feature-state;
  • избегать постоянного вызова setData;
  • ограничивать число DOM-маркеров.

Для крупных геоинформационных систем:

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

Грамотная организация рендеринга, разумное управление источниками данных и эффективное использование возможностей WebGL позволяют поддерживать плавную анимацию даже при отображении десятков тысяч объектов на карте.