A Leaflet Developer’s Guide to High-Performance Map Visualizations in React

Insights from a Leaflet Developer on React-Leaflet Performance Issue Data Visualizations

A Leaflet Developer’s Guide to High-Performance Map Visualizations in React

Are you gearing up to create a map visualisation that handles a large dataset? I’m excited to guide you through the process of crafting interactive map visualizations while sidestepping pitfalls. Drawing from my experience as a freelance React and Leaflet developer, I encountered diverse requirements for map visualizations—rendering extensive datasets, creating interactive visuals, and managing the drawing of various shapes.

As an experienced Leaflet developer, I joined the project where Leaflet was the chosen mapping library. Then, I opted to incorporate Leaflet-Geoman due to its comprehensive suite of drawing functionalities that perfectly matched our requirements. There was a B2B company as the sole client. They didn’t want to use an external service for a GIS server and back-end developers were too busy to build a self-hosting GIS server. As a front-end developer I was facing a challenge—rendering over 100k polygons on a Leaflet map while enabling seamless edits and drawing. My journey as a dedicated Leaflet developer in optimizing Leaflet rendering taught me invaluable lessons. Let’s delve into the realm of crafting performant map visualisations using Leaflet and React.

How to integrate Leaflet into React

When you google how to integrate Leaflet into React, all sources are directing to React library React Leaflet. Unfortunately, this library doesn’t perform well if you want to render a lot of objects in Leaflet. When I tried to render around 40k Geojson objects in Leaflet’s canvas mode, it took around 30 seconds to render the map. When I used Leaflet’s GeoJSON, instead of React Leaflet GeoJSONs, initial rendering took a few seconds. The response from the library maintainer was that React Leaflet is an additional abstraction and it’s expected that rendering is less performant. My first advice would be avoid leaning solely on this library.

react leaflet performance issue

But how can we use Leaflet in React without React Leaflet library? We can utilize the library source code to implement Leaflet map container and its map context. Take a look at the code snippet below:


import type {
  Layer,
  LayerGroup,
  FitBoundsOptions,
  LatLngBoundsExpression,
  MapOptions,
} from "leaflet";
import { Control, Map as LeafletMap, tileLayer } from "leaflet";
import "leaflet/dist/leaflet.css";
import type { HTMLProps } from "react";
import { useCallback, useEffect, useState } from "react";

export type ControlledLayer = {
  addLayer(layer: Layer): void;
  removeLayer(layer: Layer): void;
};

export type LeafletContextInterface = Readonly<{
  __version: number;
  map: LeafletMap;
  layerContainer?: ControlledLayer | LayerGroup;
  layersControl?: Control.Layers;
  overlayContainer?: Layer;
  pane?: string;
}>;

const CONTEXT_VERSION = 1;

function createLeafletContext(map: LeafletMap): LeafletContextInterface {
  return Object.freeze({ __version: CONTEXT_VERSION, map });
}

type DivProps = HTMLProps<HTMLDivElement>;

export interface MapContainerProps extends MapOptions, DivProps {
  bounds?: LatLngBoundsExpression;
  boundsOptions?: FitBoundsOptions;
  whenReady?: () => void;
}

export function Map({
  center = [0, 0],
  zoom = 1,
  bounds,
  boundsOptions = { padding: [500, 500], maxZoom: 21 },
  whenReady,
  ...options
}: MapContainerProps) {
  const [context, setContext] = useState<LeafletContextInterface | null>(null);

  const mapRef = useCallback((node: HTMLDivElement | null) => {
    if (node !== null && context === null) {
      const map = new LeafletMap(node, { zoomControl: false, ...options });

      tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
        attribution:
          '© OpenStreetMap contributors',
      }).addTo(map);

      if (center != null && zoom != null) {
        map.setView(center, zoom);
      } else if (bounds != null) {
        map.fitBounds(bounds, boundsOptions);
      }
      if (whenReady != null) {
        map.whenReady(whenReady);
      }

      setContext(createLeafletContext(map));
    }
  }, []);

  useEffect(() => {
    return () => {
      context?.map.remove();
    };
  }, []);

  return (
      <div ref={mapRef} />
  );
}

Using Vector Tiles in Leaflet

Most visualistions libraries use SVG by default, including Leaflet. If you need to render more than 10k objects, sluggish rendering due to bloated DOM elements becomes an obstacle. While Leaflet’s canvas mode is an option, rendering over 100k objects stretches its limits. Fortunately, there are solutions that perform very well with large amounts of data, such as WebGL rendering engines and Vector Tiles. Leaflet doesn’t support WebGL or Vector Tiles out of box, but its plugins bridge the gap.

Using SuperCluster instead of MarkerCluster

Any seasoned Leaflet developer knows that visualizing thousands of markers at a single location can overwhelm users. For this reason we need to implement clustering.

Rendering a lot of markers without clustering

Rendering a lot of markers without clustering

Leaflet.markercluster seems like a go-to solution, yet its performance falters beyond 100k markers. For a leap in performance, consider supercluster. I created a repository on Github, where you can see the Leaflet.markercluster vs supercluster benchmark test. There is a significant difference with more than 100k markers.

Markercluster result:

num of markersinitial rendering
500k6 mins
400k3 mins
300k2 mins 11 sec
250k1 min 24 sec
200k52 sec
150k30 sec

Loading 500k markers in Supercluster takes around 1-2 seconds.

Profiling and optimising

As we use functional components with React Hooks, rerendering more than 100k objects in React can be pretty expensive and slow. For this reason, React’s useMemo is your best friend. I used turf.js library for implementing spatial algorithms and functionalities. Turf.js is a great library that is very helpful, but I came across a use case where the contains function didn’t perform well. Running this function for 100k objects took several seconds. If you work with large amounts of data in React and you come across performance issues, Chrome profiler is your best friend.

Javascript profiling in Google Chrome

Javascript profiling in Google Chrome

When I was looking for alternatives and other spatial libraries, I came across @terraformer/spatial that has the contains function and it performs very fast for 100k objects.

Conclusion

In this blog post we talked about several optimizations for building high-performance map visualisation in React. There are a lot of ways and libraries for optimising rendering performance, but we need to consider other requirements and compatibility among plugins and libraries. I hope you find this blog post helpful and embark on a journey of seamless map visualisation craftsmanship. 🗺️

LOOKING FOR AN EXPERT LEAFLET DEVELOPER? LET’S BUILD SOMETHING.

GET IN TOUCH

Leave a Reply

Your email address will not be published. Required fields are marked *

%d bloggers like this: