DEV Community

Cover image for How Lemontree improved map performance with a small refactor
Chris Whong for Mapbox

Posted on

How Lemontree improved map performance with a small refactor

A Q&A with Samuel Cole, CTO of Lemontree


Lemontree is a New York-based nonprofit on a mission to fight food insecurity by connecting hungry Americans to free food resources near them. Often described as a "Yelp for food banks," they curate and manage rich data on thousands of food pantries across the U.S., making it accessible through an easy-to-use digital platform with maps, SMS alerts, and more. In 2025, Lemontree helped over a million people locate nearby food pantries.

At the heart of their product is an interactive map — powered by Mapbox GL JS — that lets users instantly see nearby pantry locations, hours, and available foods. Recently, Lemontree overhauled how they load and display map data to dramatically improve performance. We sat down with Samuel Cole, CTO of Lemontree, to learn how they approached the problem and what they'd do differently from the start.


Chris: Let's start with the original setup. What were you displaying on the map, where did the data come from, and what was falling short in terms of user experience?

Samuel: When we load a map, we first call an endpoint to find the best 10 resources for the focus point of the map using our AI sorting algorithm, but then we don't want you to miss out on the other resources in the area, so we built an API called markersWithinBounds that returns all the resources within the bounding box of the map. These are visualized as many smaller background markers in addition to the larger "best 10 resources" markers.

We're using react-map-gl so it was straightforward to loop over the results of that api call with a .map() and create a component for each one. When we first built that, we were testing pretty zoomed in to a few blocks around the location, so while the density of food pantries is pretty high (there are more food pantries in the US than McDonalds!), it seemed like it was scaling ok, because most people are only interested in the immediate area.

But in real life, pretty much everybody immediately zoomed out! All of a sudden the API call returned thousands of points, and rendering thousands of Markers on the screens was chugging. Users' whole device was lagging and slow, and then if they started panning around? It was wild.

Zooming out on the map triggered an API call for new data and could contain hundreds of points or more.


Chris: This is a classic Mapbox GL JS pitfall that catches a lot of developers by surprise. Markers are the most intuitive way to get point features on a map — the API is simple, positioning is easy, and you can drop in a custom image without much fuss. But they're rendered as individual DOM elements, which means performance degrades quickly as the count climbs. Beyond a few hundred markers, you're fighting the browser itself. After you hit this wall, what solutions did you explore, and how did you arrive at the right path forward?

Samuel: When I was preparing my talk for the BUILD with Mapbox virtual conference, you actually pulled me aside and said: "Hey, if you used a symbol layer for this it would scale a ton better."


Chris: Ah, well you were in the right place to get a direct suggestion! Hopefully this post will help a lot more people who might be hitting the same issue. Symbol layers are rendered by the GPU as part of the map's WebGL canvas, which means they handle thousands of points with ease and pan or zoom without any jank. The tradeoff is that they're less intuitive to set up, especially in a React codebase where you're used to thinking declaratively. Can you walk us through what the refactor actually looked like?

Samuel: We have a lot going on at Lemontree! I wasn't able 📍 immediately prioritize that work, but one Friday I took it on as sort of a 'hack day' project. Unfortunately, I got a bit stuck because the react-map-gl <Layer> component, which was declarative in the React style, was being added before the style was loaded, and I just ran out of time in my hack day to figure it out. When I got access to AI-assisted development through Copilot and Claude Code, I was looking for little test cases, and I remembered this performance issue: I asked Claude, it added an styleLoaded && pattern, and we were off to the races! You can compare the code for the Marker implementation vs the Symbol layer:

Before: Markers 📍

 {/* Old approach: Render a DOM marker for each resource */}
  {otherMarkersData?.features.map((feature) => (
    <Marker
      key={feature.properties.id}
      latitude={feature.geometry.coordinates[1]}
      longitude={feature.geometry.coordinates[0]}
    >
      <Image
        src={purplePin}
        alt="Resource"
        width={20}
        height={25}
        style={{ cursor: 'pointer' }}
        onClick={() => router.push(`/resources/${feature.properties.id}`)}
      />
    </Marker>
  ))}

Enter fullscreen mode Exit fullscreen mode

Problems:

  • Creates a DOM node for every marker (100s+ of elements)
  • Heavy re-renders when panning/zooming
  • Poor performance with many markers
  • Limited styling options

After: Symbol Layer 🚀

{/* New approach: Single GeoJSON source with symbol layer */}
  <Source
    id="other-resources"
    type="geojson"
    data={filteredOtherMarkersData}
    promoteId="id"
  >
    <Layer
      id="other-resources-layer"
      type="symbol"
      layout={{
        'icon-image': 'purpleMarker',
        'icon-allow-overlap': true,
      }}
    />
  </Source>
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Single GeoJSON source for all markers
  • GPU-accelerated rendering
  • Smooth performance with 1000+ markers
  • Feature state for hover effects
  • Click/hover handled via interactiveLayerIds

Performance Impact:

  • 10x faster rendering
  • Butter-smooth panning and zooming
  • Reduced memory footprint

Chris: That's a great example of AI tooling unblocking a task that kept slipping down the priority list. The styleLoaded guard is a small but critical detail — symbol layers depend on the map style being fully initialized before you can add sources or layers to the canvas at runtime, and it's an easy thing to miss until things break in subtle ways. What has the impact been since shipping? Are users noticing the difference?

Samuel: Our clients are spending a bit more time exploring around the map, but honestly our recommendation algorithm gives plenty of food pantries near where they live, so in surprising ways exploring the map isn't their primary way of discovering food resources, and they use the map more for navigating the pantries we've already referred them to. Where the map really shines is with our partners! Organizations that run many pantries love to see their whole network, foundations love to see our coverage, and governments are interested in how food access is working in their districts.


Chris: That's a fascinating insight — power users for the map include the partner organizations doing network-level planning, not just the end users trying to find nearby food. It reframes what "map performance" means for your product. Speaking of which, you've been building on Mapbox for a long time. What's your overall take on the platform — what's working well, and where do you think there's room to grow?

Samuel: We love Mapbox; I've been using maps for my whole career, ever since my very first tech startup job, and Mapbox reflects my values for open, accessible, and extensible maps. The reality is that other mapping providers just don't offer the extensibility of Mapbox's platform, whenever I want to create a more targeted experience (which I believe people experiencing food insecurity deserve the best experiences!) the out-of-the-box approach from other map providers, just isn't going to cut it. I don't want a Google Map, I want a Lemontree Map.


Chris: "I don't want a Google Map, I want a Lemontree Map" — I love that. Mapbox gives you all you need to make the whole map experience your own creation. What's on the roadmap next for mapping at Lemontree?

Samuel: I love maps! Around Lemontree, my coworkers all call me the map guy. I think as we grow, it's going to be even more important for our partners to visualize how food access works around the U.S., so I'm excited to make more visualizations to tell stories like "Where are the food pantries that focus on elderly people in New Jersey, and where are the gaps?" There are almost 50 million people who experience food insecurity in the US, and we have a lot of work to do to understand where they are, and what resources they have available to them.

Chris: Great, hopefully Mapbox maps can help with that data storytelling to raise awareness about food insecurity. Thanks for chatting with me about your map's technical hiccup and the fix. I'm glad to hear the map is performing better, especially given the importance of the data you're displaying on it.


Takeaways

Lemontree's Markers to symbol layer refactor is a great case study in a problem many Mapbox developers encounter: Markers and symbol layers are both valid tools for displaying point data, but they have very different performance profiles.

Markers are DOM elements — straightforward to set up, but expensive to render at scale. Symbol layers are drawn by WebGL on the map's canvas, making them capable of displaying tens of thousands of points without breaking a sweat. If you're showing dynamic data that can grow beyond a few hundred points, a symbol layer is almost always the right call.

These graphics help explain the difference between Markers, which are DOM elements displayed "on the map" versus symbol layers which are rendered "in the map"

For teams building in React with react-map-gl, the key implementation detail is guarding your layer additions behind a styleLoaded check — a small fix that, as Samuel found, can make the difference between a polished experience and a broken one. (the equivalent when using Mapbox GL JS without a wrapping library is listening for the map.on(style.load) event.

You can learn more about when to use Markers versus symbol layers in the Add your Data guide in the Mapbox GL JS documentation.

Top comments (0)