Skip to content

fix: re-render Layer and Source on styledata only when missing from the style - #2621

Open
tordans wants to merge 2 commits into
visgl:masterfrom
tordans:fix/styledata-rerender
Open

tordans wants to merge 2 commits into
visgl:masterfrom
tordans:fix/styledata-rerender

Conversation

@tordans

@tordans tordans commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Disclaimer: Found and fixed with Clause Opus 5.5 High. Description also written by Claude and reviewed by me.

The problem

<Layer> and <Source> subscribe to the map's styledata event and force a render on every event. MapLibre and Mapbox fire styledata for every style change: map.addImage, map.setPaintProperty, a layer that another component adds. So with 120 <Layer>s, every icon an app loads at runtime renders all 120 again, and every <Source> schedules a timer and a render.

Those renders find the layer or source in the style and call updateLayer / updateSource with unchanged props, which does nothing. The render only does useful work when the layer or source is missing from a loaded style, for example after a style swap.

The change

The styledata listener now forces a render only when the layer (map.getLayer(id)) or source (map.getSource(id)) is missing, or the style is not loaded. The subscription and the map.style._loaded check stay as they are, so the timing of when sources and layers are added is unchanged (see the reverts in #2526).

Applied to layer.ts and source.ts in react-maplibre and react-mapbox. I left the mapbox-legacy copy in modules/main alone.

Unchanged behaviour:

  • After a style swap, layers and sources are missing, so they are added again (covered by the existing Source/Layer tests).
  • A layer whose source or beforeId is not in the style yet is still missing, so it renders again on the next styledata and is added then.
  • While the style is not loaded, every event still renders.

New test in both packages: addImage + setPaintProperty no longer render <Source> or <Layer>, and a layer removed outside React is added again. It fails without the change.

Measured, before and after

120 layers on 4 GeoJSON sources (10,000 features). Each <Layer> and <Source> is wrapped in its own React.Profiler. Chrome 153 headless, Apple M4, maplibre-gl 6.14.0, React 18.3.1, median of 5 page loads per build.

Scenario before after
50 × addImage, one per frame (100 styledata) <Layer> renders 6,120 0
<Source> renders 208 0
React render time 5.6 ms 0 ms
main-thread script time 82.1 ms 68.0 ms
50 × setPaintProperty on one layer, one per frame <Layer> renders 6,000 0
<Source> renders 200 0
React render time 5.5 ms 0 ms
main-thread script time 84.5 ms 75.0 ms
mapStyle swap all 120 layers back after 25.2 ms 25.7 ms

Main-thread script time is mostly MapLibre's own work, so on a fast machine its drop varies between runs: −1 % to −17 % over four runs. The saved render time grows with the number of layers and on slower devices. In an editor with 121 layers that adds marker images on demand, one load fired 51 styledata events and 3,525 <Layer> renders; with this change those renders are gone.

Benchmark (before/after, runs with two checkouts of this repo): https://gist.github.com/tordans/964b42fdd89d39c1dfaf98d7419cf6a7

Validation: TypeScript build, yarn lint, and all 86 node/headless tests pass.


Note

Medium Risk
Touches map style sync for core Source/Layer components; behavior is narrowed but style swaps and missing-layer recovery must stay correct.

Overview
Stops <Source> and <Layer> from re-rendering on every map styledata event (e.g. addImage, setPaintProperty), which previously caused large apps with many layers to re-render unnecessarily.

The styledata handler now calls forceUpdate only when recovery is needed: <Layer> when the layer id is absent from a loaded style; <Source> when the source is missing or is not the same instance children were rendered with (sourceRef), including after a style swap or a colliding source id in the style. The same logic is applied in react-mapbox and react-maplibre.

Tests assert zero profiler renders after unrelated style mutations, that externally removed layers are re-added, and that layers still mount when the initial style already defines the source.

Reviewed by Cursor Bugbot for commit 09816b1. Bugbot is set up for automated code reviews on this repo. Configure here.

…he style

`styledata` fires on every style change, such as addImage or
setPaintProperty. Each event rendered every <Layer> and <Source> again,
although the render only does work when the layer or source is missing,
for example after a style swap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no actionable new issues were found.

Summary

Reduces unnecessary renders after map style changes in both packages.

  • Sources and layers render again only when their map entries need recovery.

Reviews (2) · Last reviewed commit: "fix: re-render Source when the style has..." · Reviewed by Greptile

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit fb15183. Configure here.

Comment thread modules/react-maplibre/src/components/source.ts
`<Source>` only renders its children once it has a source. When the
style itself defines a source with the same id and loads after the
first render, the `styledata` check found the source and skipped the
render, so the children never mounted.

Re-render whenever the source in the style is not the one the
children were rendered with.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant