Skip to content

setFeatureState causes ~1s frame time in v3 with large vector tile layers (60x regression from v2) #13624

@janneskruse

Description

@janneskruse

First of all, thanks a lot for your amazing work on mapbox! However, I noticed this bug on an old project that I am currently upgrading and where I also wanted to upgrade to the latest mapbox gl js version:

mapbox-gl-js version

v3.18.1

Browser and version

Chrome 144.0.7559.133

Expected behavior

The same performance as in v2 :D.

Actual behavior

Calling setFeatureState to toggle a single boolean ({ hover: true }) on one feature causes Mapbox GL v3 to rebuild paint arrays for all features in all visible tiles, resulting in ~800-1200ms frame times. The same code on v2.13.0 runs at 60fps (~16.7ms frames).

Performance Data

mapbox-gl v3.18.1

Metric Value
Frame time (RAF-to-RAF) 800–1,200ms (~1 FPS)
setFeatureState JS execution 0.0–0.2ms
RAF wait time 680–1,060ms
Initial feature state assignment (197K cells) 730ms

Chrome DevTools Bottom-up profiler (per frame):

Self time % Function
268ms 34.3% updatePaintArrays
242ms 30.9% eachPosition
85ms 10.8% updateBuckets
59ms 7.5% getNumericId
24ms 3.0% evaluate

Total scripting per frame: ~780ms — almost entirely in Mapbox internals rebuilding paint vertex buffers.

mapbox-gl v2.13.0 (working)

Metric Value
Frame time 16.7ms (60 FPS)

Chrome DevTools Bottom-up profiler (per frame):

Self time % Function
1.3ms 9.9% _updateLoaded
1.3ms 9.7% render
0.7ms 5.6% quadrant
0.7ms 5.2% getByKey
0.7ms 5.1% idealTiles

Analysis

The setFeatureState({ hover: true }) call on a single feature takes <0.2ms in both versions. However, in v3, the subsequent render cycle calls updatePaintArrays which iterates every vertex position (eachPosition) across all features in all visible tiles. This O(N) paint array rebuild per frame is the bottleneck.

In v2, feature-state changes appear to be handled incrementally without a full paint array rebuild, keeping frames at ~16ms.

Impact

This makes setFeatureState-based hover interactions unusable for any layer with >10K features in v3. Applications that worked smoothly on v2 become completely unresponsive after upgrading.

Link to the demonstration

No response

Steps to trigger the unexpected behavior

  1. Add a vector tile source with ~200K polygon features
  2. Add a fill layer with paint expressions referencing feature-state:
    paint: {
      'fill-opacity': [
        'case',
        ['boolean', ['feature-state', 'hover'], false], 1,
        0.7
      ]
    }
  3. Assign feature state to all features (initial data load):
    grid.forEach(row => {
      map.setFeatureState(
        { source: 'grid', sourceLayer: 'grid_d1_sq_fid', id: row.id },
        { status: row.status, color: row.color }
      );
    });
  4. On mousemove, toggle hover state on a single feature:
    map.setFeatureState(
      { source: 'grid', id: hoveredId, sourceLayer: 'grid_d1_sq_fid' },
      { hover: true }
    );
  5. Observe frame rate during hover.

Relevant log output

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions