requestAnimationFrame является базовым API браузера для
построения высокопроизводительных анимационных циклов. В отличие от
setInterval и setTimeout, вызовы
синхронизируются с частотой обновления экрана, что позволяет избегать
лишних перерисовок и снижает нагрузку на основной поток.
Основные характеристики механизма:
Базовая структура цикла выглядит следующим образом:
function loop(time) {
// логика обновления состояния
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
Mapbox GL JS использует собственный непрерывный render loop, основанный на WebGL. Карта перерисовывается при изменении состояния: перемещение камеры, обновление источников данных, изменение стилей или триггер repaint.
Внутри библиотеки применяется подход, при котором рендеринг запускается только при необходимости, но при активной анимации поддерживается непрерывный цикл кадров.
Ключевые триггеры перерисовки:
camera (center, zoom, bearing, pitch);GeoJSONSource;map.triggerRepaint();Использование собственного requestAnimationFrame поверх
Mapbox GL JS требует учета уже существующего рендер-цикла. Дублирование
полной перерисовки карты приводит к избыточной нагрузке, поэтому чаще
используется стратегия “внешнего обновления состояния” без
принудительного рендера.
Типовой паттерн:
function animate() {
updateState(); // обновление данных слоя, маркеров, анимации
map.triggerRepaint(); // сигнал карте на перерисовку
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
map.triggerRepaint() является ключевым механизмом
интеграции: он не рисует кадр напрямую, но сообщает WebGL-пайплайну о
необходимости обновления.
Часто анимация зависит от текущего состояния камеры:
Синхронизация достигается через чтение состояния карты внутри RAF-цикла:
function animateMarkers() {
const center = map.getCenter();
const zoom = map.getZoom();
updateMarkers(center, zoom);
requestAnimationFrame(animateMarkers);
}
requestAnimationFrame(animateMarkers);
Такой подход используется при построении динамических визуализаций, где объекты зависят от географического контекста.
Mapbox GL JS поддерживает пользовательские WebGL-слои через интерфейс
customLayer. В этом случае RAF может использоваться для
управления внутренним состоянием слоя, но рендеринг выполняется через
жизненный цикл карты (render callback).
Структура custom layer:
const animatedLayer = {
id: 'animated-layer',
type: 'custom',
renderingMode: '2d',
onAdd(map, gl) {
this.map = map;
this.gl = gl;
this.angle = 0;
},
render(gl, matrix) {
this.angle += 0.01;
drawSomething(gl, matrix, this.angle);
this.map.triggerRepaint();
}
};
В этом сценарии использование внешнего
requestAnimationFrame часто избыточно, поскольку Mapbox уже
управляет циклом отрисовки.
Одним из распространённых применений является анимация данных в
GeoJSONSource.
Пример обновления координат:
const source = map.getSource('points');
let t = 0;
function animate() {
t += 0.01;
const features = [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [
baseLng + Math.cos(t) * 0.1,
baseLat + Math.sin(t) * 0.1
]
}
}
];
source.setData({
type: 'FeatureCollection',
features
});
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
Каждое обновление источника вызывает перерасчёт слоя и WebGL-перерисовку.
При высокой частоте RAF возможны избыточные вычисления, особенно при работе с большими наборами данных. Применяются стратегии:
Пример временного ограничения:
let lastTime = 0;
function animate(time) {
if (time - lastTime > 16) {
updateHeavyCalculations();
lastTime = time;
}
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
Mapbox GL JS предоставляет события жизненного цикла карты, которые часто используются для управления запуском и остановкой анимации:
loadrenderidlemovezoomПример условного запуска цикла:
let running = false;
function loop() {
if (!running) return;
updateAnimationState();
map.triggerRepaint();
requestAnimationFrame(loop);
}
map.on('move', () => {
if (!running) {
running = true;
requestAnimationFrame(loop);
}
});
map.on('idle', () => {
running = false;
});
Такой подход снижает нагрузку при отсутствии активности.
Помимо WebGL-слоёв, часто используется HTML-оверлей, синхронизированный с координатами карты:
const el = document.createElement('div');
el.className = 'marker';
document.body.appendChild(el);
function update() {
const pos = map.project([lng, lat]);
el.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
requestAnimationFrame(update);
}
requestAnimationFrame(update);
Проекция координат через map.project должна выполняться
с учетом текущего состояния камеры, что делает RAF необходимым для
плавности.
Mapbox GL JS использует внутренний цикл, оптимизированный под WebGL контекст. При внешнем RAF важно учитывать:
triggerRepaint увеличивают GPU нагрузку;source.setData вызывает полный пересчёт
слоя;Рациональная модель:
triggerRepaint служит сигналом, а не командой
отрисовки.При наличии нескольких независимых анимаций применяется единый глобальный RAF:
const systems = [];
function register(fn) {
systems.push(fn);
}
function loop() {
for (const fn of systems) {
fn();
}
map.triggerRepaint();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
Такой подход снижает количество RAF-циклов и упрощает координацию обновлений.
При изменении zoom уровень детализации часто должен меняться синхронно с анимацией. RAF позволяет динамически адаптировать вычисления:
function animate() {
const zoom = map.getZoom();
const complexity = zoom > 10 ? 1 : 0.5;
updateVisualization(complexity);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
Управление сложностью вычислений в зависимости от масштаба снижает нагрузку на GPU и CPU при низких уровнях zoom.