Short answer

A slow web GIS is almost always slow for one of three measurable reasons: too much data crossing the network, too much geometry being parsed on the main thread, or too many draw calls hitting the renderer. The fix is to give each layer a budget — payload size, vertex count, and render path — and choose the delivery format to fit it: GeoJSON for small static overlays, vector tiles for large multi-scale vector data, COGs or raster tiles for imagery, and GPU layers (deck.gl) when you need to draw hundreds of thousands of features at once. Performance is not a polish step; it is a data-preparation decision made before the first line of map code.

The three budgets

Frame every layer against three limits and you can diagnose any slow map.

  1. Network payload. Bytes the browser must download before it can draw. A 30 MB GeoJSON is 30 MB on every page load. Aim to keep initial map payload to a few megabytes and stream the rest.
  2. Main-thread parse and memory. GeoJSON is parsed into JS objects on the UI thread; a large file freezes interaction while it parses and then holds every coordinate in memory. This is why a map can be "loaded" yet stutter on pan.
  3. Render / draw cost. How much geometry the renderer rasterises each frame. MapLibre's renderer is efficient but still scales with visible vertices; deck.gl pushes work to the GPU so it scales far higher.

A map that misses its 60 fps target (16.7 ms/frame) is overspending one of these three. Browser dev tools (Network tab for payload, Performance tab for long tasks and frame rate) tell you which.

Vector tiles vs GeoJSON

The biggest single lever is choosing tiles over a monolithic file at the right threshold.

  • GeoJSON is excellent for small, static overlays — a few hundred to a few thousand simple features, a few hundred kilobytes. It is simple, debuggable, and needs no tile server.
  • Vector tiles (Mapbox Vector Tiles / .pbf, served as {z}/{x}/{y} or from a PMTiles archive) deliver only the features in the current viewport, generalised to the current zoom. The client never holds the whole dataset. This is the right choice once a layer grows past a few thousand complex features, exceeds a few megabytes, or spans many zoom levels.

Generate tiles with tippecanoe, which handles zoom-dependent simplification and feature dropping:

tippecanoe -o geology.pmtiles -Z4 -z14 \
  --drop-densest-as-needed \
  --simplification=10 \
  --coalesce-densest-as-needed \
  -l units geology.geojson

-Z4 -z14 sets the min/max zoom; --drop-densest-as-needed keeps tile sizes under control in crowded areas; --simplification trims vertices at low zoom where detail is invisible anyway. PMTiles gives you a single file served by range requests — no tile-server process required.

Simplify before you serve

Detail that a pixel cannot show is pure cost. Geological linework digitised at 1:5,000 carries far more vertices than a zoom-6 view can render.

  • Pre-simplify with Douglas-Peucker (Visvalingam preserves shape better for natural features): QGIS Simplify, ogr2ogr -simplify, or PostGIS ST_SimplifyVW(geom, tolerance). Match tolerance to the smallest zoom that uses the layer.
  • Reduce coordinate precision. Six decimal degrees is ~0.11 m precision; more than that is wasted bytes. ogr2ogr -lco COORDINATE_PRECISION=6 or round in PostGIS.
  • Drop attributes not used for styling or interaction before export; they inflate every feature.

Rasters: COGs and overviews

For imagery, DEMs, and hillshades, never serve a plain GeoTIFF to the browser. Use a Cloud Optimized GeoTIFF: internally tiled, with overviews (pyramids), so a client can fetch just the tiles and resolution it needs via HTTP range requests.

gdal_translate input.tif cog.tif \
  -of COG -co COMPRESS=DEFLATE -co PREDICTOR=2 \
  -co BLOCKSIZE=512 -co OVERVIEW_RESAMPLING=BILINEAR

For styled basemap-like raster, pre-rendering raster tiles (XYZ) is still the lightest option for the client. Either way, the principle is the same as vector tiles: deliver only the viewport at only the needed resolution.

When to reach for the GPU (deck.gl)

MapLibre comfortably renders thousands to low tens of thousands of vector features. Past that — dense point clouds, large trajectory sets, animated layers — move to deck.gl, which renders on the GPU and can draw hundreds of thousands to millions of points (ScatterplotLayer), aggregate them on the fly (ScreenGridLayer, HexagonLayer), or render 3D and large terrain. deck.gl interleaves with MapLibre so you keep the basemap and styling while offloading the heavy layer to WebGL. Use it as a targeted tool for the one layer that needs it, not as a default — it adds bundle size and complexity.

A performance checklist

Run this before shipping an interactive map:

  1. Budget each layer: expected payload, feature/vertex count, and render path. Flag anything over a few MB or tens of thousands of features.
  2. Pick the format per layer: GeoJSON (small/static), vector tiles/PMTiles (large/multi-scale vector), COG or raster tiles (imagery/DEM), deck.gl (very high feature counts or 3D).
  3. Simplify and trim: zoom-appropriate geometry simplification, 6-decimal coordinate precision, drop unused attributes.
  4. Tile with zoom limits matched to actual use; verify tile sizes stay reasonable (tippecanoe reports them).
  5. Lazy-load: load layers on demand, not all at startup; cluster dense points (MapLibre clustering or deck.gl aggregation).
  6. Test on a real device and network: a mid-range phone on throttled 4G, with production-scale data — not a sample on a workstation.
  7. Measure: Performance panel for long tasks and frame rate; Network panel for payload and request counts.
  8. Keep uncertainty visible even after optimisation — simplified geometry must not be presented as survey-grade; carry source, scale, and confidence into popups and legends.

Common pitfalls and why they happen

  • Shipping desktop files to the browser. A full-resolution Shapefile or GeoTIFF dumped into a web map blocks the main thread on parse. Web delivery is a separate preparation step, not an export.
  • One giant GeoJSON for everything. Convenient to build, fatal to performance — every byte loads up front and lives in memory. Split and tile.
  • Over-detailed geometry at low zoom. Vertices below pixel size cost render time for no visible benefit; simplify per zoom.
  • 3D for impressiveness. A globe or extruded view adds GPU and cognitive cost when users actually need a clear 2D comparison.
  • Testing only on the developer's machine. Fast hardware and LAN hide the payload and parse problems users hit on phones and mobile networks.

Bathyl perspective

We treat interactive visualisation as making Earth data more inspectable, which means it has to load and respond on real devices, not just look right in a demo. A fast map and an honest map are the same project: deliver only what the viewport needs, render it on the right path, and keep source, scale, and uncertainty attached so the smooth, quick interface never quietly oversells the data.

Related reading

Sources