Skip to content

[Bug]: dense onCameraChanged traffic during gestures can overwhelm JS without a native throttle #4192

@ciospettw

Description

@ciospettw

Mapbox Version

11.20.1

React Native Version

0.83.2

Platform

  • iOS
  • Android

@rnmapbox/maps version

10.3.0

Standalone component to reproduce

import React, { useMemo, useState } from 'react';
import { Camera, CircleLayer, MapView, ShapeSource } from '@rnmapbox/maps';

function buildPoints(count) {
  return {
    type: 'FeatureCollection',
    features: Array.from({ length: count }, (_, index) => {
      const row = Math.floor(index / 100);
      const column = index % 100;
      return {
        type: 'Feature',
        geometry: {
          type: 'Point',
          coordinates: [12.2 + column * 0.01, 41.6 + row * 0.01],
        },
        properties: { id: String(index) },
      };
    }),
  };
}

export default function BugReportExample() {
  const [cameraChangedCount, setCameraChangedCount] = useState(0);
  const [mapIdleCount, setMapIdleCount] = useState(0);
  const shape = useMemo(() => buildPoints(5000), []);

  return (
    <MapView
      style={{ flex: 1 }}
      onCameraChanged={() => {
        setCameraChangedCount((count) => count + 1);
      }}
      onMapIdle={() => {
        setMapIdleCount((count) => count + 1);
      }}
    >
      <Camera centerCoordinate={[12.4964, 41.9028]} zoomLevel={7} />
      <ShapeSource id="dense-points" shape={shape}>
        <CircleLayer
          id="dense-point-layer"
          style={{
            circleRadius: 3,
            circleColor: '#ff4d4d',
          }}
        />
      </ShapeSource>
    </MapView>
  );
}

Observed behavior and steps to reproduce

  1. Render a map with a dense source and attach non-trivial JS work to onCameraChanged.
  2. Pinch-zoom repeatedly.
  3. onCameraChanged fires on every native camera tick on both platforms.
  4. Even if the heavy work is app-side, there is currently no native throttle for this event, so gesture-driven zoom can overwhelm the JS thread faster than onMapIdle can settle.

In practice this shows up as zoom stutter in apps that recompute visible data, declutter symbols, or trigger viewport-based fetches during camera changes.

Expected behavior

There should be an opt-in way to reduce onCameraChanged event volume natively while keeping the default behavior unchanged for existing consumers.

A cameraChangedThrottleInterval prop on MapView would let apps trade off intermediate camera granularity against JS load, while still using onMapIdle for the final settled camera state.

Notes / preliminary analysis

  • This is not a request to change the default event semantics.
  • The change is backward compatible if the prop defaults to 0 and preserves current behavior when omitted.
  • The API shape is consistent with the existing debounce/timing-related MapView props.
  • A small patch that adds cameraChangedThrottleInterval?: number on the JS side and throttles native cameraChanged dispatch on Android and iOS appears sufficient.

Additional links and references

  • A PR with an opt-in implementation and example update will be linked from this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions