Leaflet to Mapbox GL


TL;DR

The Good

  • Snappy performance after initial load
  • Lower network traffic after initial page load
  • Vector tile goodness

The Bad

  • Initial page load is harsh
  • Go ZXY or go home

The Ugly

  • Adding a marker is less abstract than it should be
  • It forgets everything if you switch styles

DR

I’ve been moving GeoPortal from Leaflet to Mapbox GL JS over the last few days. Now that I’m in the everything-kinda-works stage, I thought a post on the experience was in order.

Caveat emptor: I’m migrating this app from Leaflet. Leaflet, by my estimation, is the finest mapping library ever created. When I say Bad or Ugly, I’m judging Mapbox GL JS using a really high bar. I’m comparing a young library to a mature one, which isn’t completely fair. GeoPortal only has simple needs - interactive map, markers, overlays. I’m more familiar with Leaflet, so there’s a good chance I’m doing something wrong.

All said and done, even though I had some issues with Mapbox GL JS, I’m sticking with it.

Now for some benchmarks. These aren’t lab-clean: requests are going across the wire. I did three tries for each and averaged them, because science. Load means the load event is fired - the web page resource and its dependent resources have finished loading. Finish includes asynchronously loading objects/elements on the page which may continue downloading after the load event has fired (think march of the tiles).

Leaflet Performance
Not Cached Cached Zoom Tight
Requests 19 18 +65
Load 657ms 291ms
Finish 788ms 314ms
Size 638kb 5.1kb +2.2mb
Mapbox GL Performance
Not Cached Cached Zoom Tight
Requests 26 25 +31
Load 757ms 522ms
Finish 1.27s 845ms
Size 1.1mb 5.2kb +793kb

The Good

Mapbox GL JS brings the vector tile goodness. The benefits of vector tiles have been trumpeted so thoroughly and often I won’t pile on, but in a word, damn. Once you get started with vector tiles, not using them feels dirty.

Once you get past initial page load (more on that later), performance is extremely snappy. When flying to a location, it feels like you’re flying - no pixelated rasters going on here. Rotating and pitching the map are also fast and fun, though more in a only GIS nerds would do this kind of way.

Network traffic also goes down when interacting with the map after initial page load. Zooming tightly to the same location cost us 65 requests and 2.2mb in Leaflet, but only cost 31 requests and 793kb in Mapbox GL JS. Because vectors scale perfectly, you only need tiles down to the level of feature complexity. Generally that’s zoom level 14. Zooming in past 14 in the same area doesn’t require fetching any more tiles. The effect of that is a noticeable speed up when interacting with the map.

The Bad

Initial page load with Mapbox GL JS hurts. Initial page load size is almost twice as big over the wire vs Leaflet (1.1mb vs 638kb), and page load time also nearly doubles (1.27s vs 788ms). All things considered, those numbers aren’t terrible. The average web page size has hit the 2mb marker, and for GIS web apps, 1.27s is nothing to sneeze at. But beating 1s is my preferred metric, and you can feel the page load difference.

A more minor gripe is a lack of WMS support. If you want tiles, it’s ZXY or go home. While I find working with WMS URLs as unpleasant as the next person, it’s still a widely used standard. This isn’t an unsurmountable problem. I made a go-between proxy with a tiny amount of code. But if I can translate ZXY to WMS in a few lines of code, surely this can be built in without much fuss.

The Ugly

One of the most common things you want to do with a map is add a marker. As with almost everything in Leaflet, it’s comically simple.

1
L.marker([51.5, -0.09]).addTo(map).bindPopup("Bonus popup yo.");

In Mapbox GL JS, not so much. You have to build GeoJSON with your point in it, add that as a source to the map, and then add and style a layer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// add geojson data as a new source
map.addSource("symbols", {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"label": "my stupid marker"
},
"geometry": {
"type": "Point",
"coordinates": [
-91.395263671875,
-0.9145729757782163

]
}
}
]
}
});

// add source as a layer and apply some styles
map.addLayer({
"id": "symbols",
"interactive": true,
"type": "symbol",
"source": "symbols",
"layout": {
"icon-image": "marker-15"
},
"paint": {}
});

So…yeah. It works, but this needs to be abstracted. A lot.

Switching map styles is also unexpectedly tricky. When you add stuff to the map post style load in Mapbox GL JS, those things are added to the style. Makers, overlay layers, etc. - it’s all really one thing to Mapbox GL JS.

So when you want to setStyle() to change to a different style - say switching from a road map to a satellite map - a new style is loaded, and everything else you did post-style load is forgotten. Added a marker? Gone. Added some overlay layers? Gone. You added that to the old style, and that style is toast. So you end up with ugly stuff like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
this.map.on('style.load', function () {
this.pastInitialLoad = true;
this.map.addSource("markers", {
"type": "geojson",
"data": markers }
);
this.map.addLayer({
"id": "markers",
"type": "symbol",
"source": "markers",
"interactive": true,
"layout": {
"icon-image": "{marker-symbol}"
}
});
if (this.layers) {
this.overlayLayer(this.layers);
}
}.bind(this));

Essentially you have to pocket anything you added, create a style.load event callback, and add it all back again. I understand what’s happening, but it’s still a pain in the ass.