Field Notes

Rethinking Marine Maps for Donia: Rendering Heavy GeoJSON Layers on Mobile with MapLibre

Jul '25

When I first looked at Donia’s codebase, it felt like opening a museum.

The app was originally built in Objective-C and Java, using an old Mapbox SDK that isn’t even supported anymore. It still worked — somehow — but every feature felt fragile. The map rendering pipeline was locked in an era where devices were slower, SDKs were proprietary, and debugging meant praying Xcode didn’t freeze.

So when the team started thinking about the future — new maps, new UX, better offline mode — it was the right time to rethink everything.

The Problem: A Heavy Map in a Legacy Stack

Donia is not a simple map app.

It visualizes marine ecosystems, anchoring zones, bathymetry, and protected areas across the Mediterranean — all based on large GeoJSON and raster datasets.

On iOS, the old Objective-C Mapbox SDK had stopped being maintained, meaning we couldn’t update dependencies or use new layers. The Android version wasn’t much better.

We had two main problems:

  • The SDK was obsolete.
  • The datasets were huge.

Even a simple zoom or pan could choke the rendering thread, and adding clustering or 3D made the UX laggy.

The Approach: Rebuilding with MapLibre and Open Data

Instead of staying in a dead ecosystem, I wanted to prove we could render all this data smoothly using open tools.

So I built a small React Native mini-app powered by Expo + MapLibre + OpenStreetMap.

MapLibre is a fork of Mapbox GL, but open-source and still actively maintained — perfect for experimentation.

The goal was simple:

  • Load heavy GeoJSON layers (bathymetry, seabed zones, harbors, etc.)
  • Support offline caching
  • Stay under 100MB total app size
  • Keep the map buttery smooth

Rendering Bathymetry and Marine Layers

Bathymetry data (underwater elevation) is one of the heaviest layers — tens of thousands of coordinates.

To handle this efficiently, I:

  • Preprocessed the GeoJSON into vector tiles with smaller chunks
  • Used simplify-geojson to reduce complexity based on zoom level
  • Lazy-loaded layers only when the user zoomed into relevant depth ranges

This avoided the “everything loads at once” trap that was killing performance before.

if (zoom > 10) {
  map.addLayer(bathymetryLayer);
} else {
  map.removeLayer(bathymetryLayer);
}

Simple, but effective. The map felt instantly lighter, and we could finally navigate without stutter.

Clustering and Interactivity

Marine data also means hundreds of data points — moorings, anchorages, dive spots.

Rendering all of them individually is not an option.

MapLibre’s native clustering helped, but I also added a custom GeoJSON indexing layer with supercluster on the JS side, to precompute clusters offline.

That gave more control over appearance and performance across devices.

Going Offline: The OpenStreetMap + Cache Combo

For marine use, connectivity is unreliable.

So I implemented a caching layer using react-native-fs + MapLibre’s tile cache, storing both tiles and GeoJSON chunks locally.

Offline loading worked like this:

  • User opens a zone → app downloads all needed tiles + data layers
  • Files are cached in the app folder
  • When offline, MapLibre serves directly from local storage

It’s not a full offline vector tile server, but it’s practical — and it worked seamlessly during tests.

Lessons Learned

  • Open-source mapping is ready for production, but you need to be smart about preprocessing and caching.
  • React Native + MapLibre can handle complex layers, but you must control data flow aggressively.
  • Performance tuning beats UI sugar — smooth pans and quick zooms do more for user experience than fancy animations.
  • Legacy SDKs are technical debt with an expiry date. If your map stack depends on one, plan the migration early.

I didn’t set out to rebuild Donia — just to prove we could make it faster and simpler with modern tools.

But this small POC made one thing clear: marine maps don’t have to feel heavy.

With the right stack, even the Mediterranean can fit in your pocket.