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
- Add a vector tile source with ~200K polygon features
- Add a
fill layer with paint expressions referencing feature-state:
paint: {
'fill-opacity': [
'case',
['boolean', ['feature-state', 'hover'], false], 1,
0.7
]
}
- 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 }
);
});
- On
mousemove, toggle hover state on a single feature:
map.setFeatureState(
{ source: 'grid', id: hoveredId, sourceLayer: 'grid_d1_sq_fid' },
{ hover: true }
);
- Observe frame rate during hover.
Relevant log output
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
setFeatureStateto 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
setFeatureStateJS executionChrome DevTools Bottom-up profiler (per frame):
updatePaintArrayseachPositionupdateBucketsgetNumericIdevaluateTotal scripting per frame: ~780ms — almost entirely in Mapbox internals rebuilding paint vertex buffers.
mapbox-gl v2.13.0 (working)
Chrome DevTools Bottom-up profiler (per frame):
_updateLoadedrenderquadrantgetByKeyidealTilesAnalysis
The
setFeatureState({ hover: true })call on a single feature takes <0.2ms in both versions. However, in v3, the subsequent render cycle callsupdatePaintArrayswhich 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
filllayer with paint expressions referencingfeature-state:mousemove, toggle hover state on a single feature:Relevant log output