Throttling и debouncing

При работе с CesiumJS часто возникает необходимость обрабатывать частые события, не перегружая основной поток вычислений и не вызывая избыточные перерисовки сцены. Особенно это заметно при работе с камерой, обработчиками мыши, событиями Scene.postRender, изменениями позиции курсора и динамическими обновлениями сущностей. В таких сценариях применяются техники контроля частоты вызова функций — throttling и debouncing.

CesiumJS активно использует событийную модель. Камера может изменять положение десятки раз в секунду, курсор генерирует поток mousemove, а рендер-цикл сцены работает на высокой частоте (обычно 60 FPS).

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

  • перегрузка JavaScript-движка;
  • рост времени обработки одного кадра;
  • просадки FPS при сложных сценах;
  • избыточные пересчёты координат (Cartesian3 ↔︎ Cartographic);
  • лишние обновления сущностей (Entity, Primitive);
  • деградация интерактивности интерфейса.

CesiumJS особенно чувствителен к этим проблемам при работе с:

  • ScreenSpaceEventHandler
  • Scene.camera.changed
  • Scene.postRender
  • Clock.onTick
  • динамическими Entity.position

Throttling: ограничение частоты вызовов

Throttling (троттлинг) ограничивает выполнение функции так, чтобы она вызывалась не чаще одного раза за заданный интервал времени.

Идея заключается в равномерном распределении вызовов во времени.

Базовая реализация throttling

function throttle(fn, limit) {
    let inThrottle = false;
    let lastArgs = null;

    return function (...args) {
        if (!inThrottle) {
            fn.apply(this, args);
            inThrottle = true;

            setTimeout(() => {
                inThrottle = false;
                if (lastArgs) {
                    fn.apply(this, lastArgs);
                    lastArgs = null;
                }
            }, limit);
        } else {
            lastArgs = args;
        }
    };
}

Применение в CesiumJS: обработка движения камеры

Одна из частых задач — обновление UI при изменении положения камеры.

const viewer = new Cesium.Viewer("cesiumContainer");

function updateCameraInfo() {
    const camera = viewer.camera;
    const cartographic = Cesium.Cartographic.fromCartesian(camera.position);

    const lon = Cesium.Math.toDegrees(cartographic.longitude);
    const lat = Cesium.Math.toDegrees(cartographic.latitude);

    console.log("Camera:", lat, lon);
}

viewer.camera.changed.addEventListener(
    throttle(updateCameraInfo, 200)
);

Без throttling событие camera.changed может вызываться десятки раз за секунду, что приводит к лишним вычислениям преобразования координат.


Throttling в обработке движения мыши

const handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas);

function handleMouseMove(movement) {
    const cartesian = viewer.camera.pickEllipsoid(
        movement.endPosition
    );

    if (!cartesian) return;

    const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
    const lon = Cesium.Math.toDegrees(cartographic.longitude);
    const lat = Cesium.Math.toDegrees(cartographic.latitude);

    console.log(lat, lon);
}

handler.setInputAction(
    throttle(handleMouseMove, 100),
    Cesium.ScreenSpaceEventType.MOUSE_MOVE
);

Когда throttling особенно эффективен

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

Debouncing: задержка выполнения до стабилизации

Debouncing (дебаунсинг) откладывает выполнение функции до тех пор, пока поток вызовов не прекратится на заданное время.

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

Базовая реализация debouncing

function debounce(fn, delay) {
    let timer = null;

    return function (...args) {
        clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, args);
        }, delay);
    };
}

Debouncing в CesiumJS: поиск и геокодинг

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

function searchLocation(query) {
    console.log("Поиск:", query);

    // имитация запроса к API
}

const debouncedSearch = debounce(searchLocation, 400);

document.getElementById("searchInput").addEventListener(
    "input",
    (e) => debouncedSearch(e.target.value)
);

Debouncing при обновлении камеры

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

function onCameraStopped() {
    console.log("Камера стабилизировалась");

    const center = viewer.camera.pickEllipsoid(
        new Cesium.Cartesian2(
            viewer.canvas.width / 2,
            viewer.canvas.height / 2
        )
    );

    if (!center) return;

    const cartographic = Cesium.Cartographic.fromCartesian(center);
    const lon = Cesium.Math.toDegrees(cartographic.longitude);
    const lat = Cesium.Math.toDegrees(cartographic.latitude);

    console.log("Центр карты:", lat, lon);
}

viewer.camera.changed.addEventListener(
    debounce(onCameraStopped, 300)
);

Сравнение поведения throttling и debouncing в CesiumJS

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

  • Throttling подходит для:

    • live-индикаторов;
    • потокового обновления координат;
    • визуальных эффектов;
    • мониторинга камеры.
  • Debouncing подходит для:

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

Использование в рендер-цикле CesiumJS

CesiumJS предоставляет низкоуровневое событие Scene.postRender, которое вызывается каждый кадр. Без контроля частоты оно легко становится источником перегрузки.

Throttling внутри postRender

function heavyComputation() {
    console.log("Обновление аналитики сцены");
}

const throttledCompute = throttle(heavyComputation, 500);

viewer.scene.postRender.addEventListener(() => {
    throttledCompute();
});

Debouncing при обработке изменения масштаба

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

function updateLOD() {
    const height = viewer.camera.positionCartographic.height;

    if (height > 1000000) {
        console.log("Низкий уровень детализации");
    } else {
        console.log("Высокий уровень детализации");
    }
}

viewer.camera.changed.addEventListener(
    debounce(updateLOD, 250)
);

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

В реальных проектах CesiumJS часто требуется сочетание обоих подходов.

Пример: throttling + debouncing

const throttledMouse = throttle((pos) => {
    console.log("Промежуточные координаты:", pos);
}, 100);

const debouncedMouseEnd = debounce((pos) => {
    console.log("Финальные координаты:", pos);
}, 300);

const handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas);

handler.setInputAction((movement) => {
    const cartesian = viewer.camera.pickEllipsoid(movement.endPosition);
    if (!cartesian) return;

    throttledMouse(cartesian);
    debouncedMouseEnd(cartesian);
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

Типичные ошибки при использовании в CesiumJS

Частые проблемы, возникающие при неправильной реализации:

  • использование debounce там, где требуется непрерывное обновление камеры;
  • отсутствие привязки this при работе с методами Cesium объектов;
  • создание новых throttled/debounced функций внутри событийного цикла;
  • игнорирование destroy() у ScreenSpaceEventHandler;
  • чрезмерно маленькие интервалы (5–10 мс), не дающие эффекта оптимизации.

Интеграция с архитектурой приложения CesiumJS

В крупных приложениях CesiumJS функции throttling и debouncing обычно выносятся в отдельный слой утилит:

export const throttle = (fn, limit) => {
    let inThrottle = false;
    return function (...args) {
        if (!inThrottle) {
            fn.apply(this, args);
            inThrottle = true;
            setTimeout(() => (inThrottle = false), limit);
        }
    };
};

export const debounce = (fn, delay) => {
    let timer;
    return function (...args) {
        clearTimeout(timer);
        timer = setTimeout(() => fn.apply(this, args), delay);
    };
};

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


Поведение при высокой нагрузке сцены

При работе с большим количеством Entities, Billboards, Polylines и динамическими источниками данных:

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

В сценах с тысячами объектов это напрямую влияет на стабильность FPS и отзывчивость интерфейса.