React: how to optimize the performance of an Interactive Map
Interactive maps are powerful tools for improving the user experience, but managing large amounts of data without compromising performance can be a challenge. In this article, we explore effective strategies for optimising a map in React, reducing rendering load and improving scalability.
Vito Russo
Frontend JuniorReact and interactive maps
In recent years, more and more web applications and websites have integrated map-based features—real estate platforms, retail store locators, rental services, food delivery apps, and so on.
This is because navigating through a simple list of search results can be inefficient and time-consuming. Sometimes, users need key information at a glance; other times, the geographic location of a result or its distance from a specific point of interest is crucial.
In these cases, integrating a map into your web application can significantly enhance the user experience.
But how do you go about it? What challenges come with implementing a map in a web app?
In this article, I'll walk you through some key optimizations that proved useful in a similar scenario.
The Development Environment
While this article focuses on strategy and best practices, I’ll reference some technologies used in my implementation, such as React, TypeScript, Redux, and the Pigeon Maps library.
The Requirement
For a web application, we needed to implement a map displaying "pins" (icons) representing various points of interest. Each pin had to provide basic information through its icon and show additional details on mouse hover.
Think of how real estate platforms like Zillow or Airbnb visualize property listings on a map.
Possible Solutions
There are different approaches depending on the nature of the application and the volume of data.
If the number of points of interest is small and unlikely to grow significantly, a simple solution is to fetch the data from the backend and display all pins on the map at once.
This method is easy to implement—just follow the documentation of the mapping library of your choice.
However, this approach isn’t scalable. What happens if we need to display thousands or even tens of thousands of points of interest?
- If many pins are clustered together, will they overlap when zooming out?
- How long will it take to render all the pins? Keep in mind that a standard approach renders all points of interest—even those outside the user’s current view!
The Solution
To ensure scalability and maintain performance, we opted for clustering nearby pins. At lower zoom levels (zoomed out), pins are aggregated; as the user zooms in, they progressively break apart.
In mapping terms, zoom level 1 represents a global view, while zoom level 20 focuses on a single building.
To achieve this, we defined distance thresholds for each zoom level: as the zoom increases, the clustering threshold decreases.
For example:
- At zoom 13, pins within 100 km of each other merge into a single marker.
- At zoom 14, the threshold drops to 50 km, and so on.
The specific thresholds and how clustered points are represented depend on the dataset and the application. For example, aggregated pins might display summary data like the average price, the lowest price, or the presence of a key point of interest.
To manage this logic, we used utility functions to:
- Calculate the distance between two pins
- Group pins based on defined thresholds
- Determine the median position for each group
If you don’t want to build everything from scratch, you can find existing resources online. Here’s an example implementation:
export interface IGetDistanceBetweenPoints {
latitude1: number;
longitude1: number;
latitude2: number;
longitude2: number;
}
//it defines the type of each point on the map
export interface IPosition {
i: number;
lt: number | null;
ln: number | null;
}
// it defines the type of each group of points
export interface IPositionGroup {
count: number;
members: IPosition[];
lt: number;
ln: number;
}
export function getDistanceBetweenPoints({
latitude1,
longitude1,
latitude2,
longitude2,
}: IGetDistanceBetweenPoints) {
const theta = longitude1 - longitude2;
const distance =
60 *
1.1515 *
(180 / Math.PI) *
Math.acos(
Math.sin(latitude1 * (Math.PI / 180)) *
Math.sin(latitude2 * (Math.PI / 180)) +
Math.cos(latitude1 * (Math.PI / 180)) *
Math.cos(latitude2 * (Math.PI / 180)) *
Math.cos(theta * (Math.PI / 180))
);
return Math.round(distance * 1.609344);
}
export function groupPointsByDistance(points: IPosition[], threshold: number) {
// creates an empty array that keeps track of points already in a group
const alreadyInGroup: IPosition[] = [];
// creates a container array for all the groups
const groups: IPosition[][] = [];
// iterates over the points array
for (let i = 0; i < points.length; i++) {
// checks if the alreadyInGroup doesn't array contains the id of the current point
if (i === 0 || alreadyInGroup.every((el) => el.i !== points[i].i)) {
// adds the first point to the group (representing the current group) and alreadyInGroup arrays
const group = [points[i]];
alreadyInGroup.push(points[i]);
// iterates of the remaining points
for (let j = i + 1; j < points.length; j++) {
// if the current point is not included in the alreadyInGroup array
// calculates the distance between the current point and the first poin
// if the distance is below the threshold, it adds the current point to the same group
if (
alreadyInGroup.every((el) => el.i !== points[j].i) &&
getDistanceBetweenPoints({
latitude1: points[i].lt as number,
longitude1: points[i].ln as number,
latitude2: points[j].lt as number,
longitude2: points[j].ln as number,
}) < threshold
) {
group.push(points[j]);
alreadyInGroup.push(points[j]);
}
}
// finally it adds the current group to the groups array
groups.push(group);
}
}
return groups;
}
// function that returns the median point of a group of points
export function getMedianPoint(group: IPosition[]) {
// optional, it avoids that different points with the same coordinates have
// a heavier impact on the final position
const deduplicatedGroup: IPosition[] = [];
for (let i = 0; i < group.length; i++) {
if (
!deduplicatedGroup.some(
(el) => el.lt === group[i].lt && el.ln === group[i].ln
)
) {
deduplicatedGroup.push(group[i]);
}
}
// iterates over the deduplicatedGroup (made by unique coordinates points)
// and adds its coordinates to the latitude and longitude variables
let latitude = 0;
let longitude = 0;
for (let i = 0; i < deduplicatedGroup.length; i++) {
latitude += deduplicatedGroup[i].lt as number;
longitude += deduplicatedGroup[i].ln as number;
}
// returns the total count of the group and calculates the final lt and ln
return {
count: group.length,
members: group,
lt: latitude / deduplicatedGroup.length,
ln: longitude / deduplicatedGroup.length,
} as IPositionGroup;
}
export const checkIfSameCoords = (group: IPositionGroup) => {
return group.members.every((cur) =>
group.members.every((pos) => pos.ln === cur.ln && pos.lt === cur.lt)
);
};
This clustering strategy reduces the number of rendered pins, preventing overlap and performance slowdowns.
Optimizing Rendering Performance
However, there’s another challenge: many mapping libraries (including Pigeon Maps) render the entire world map along with all points of interest at once. This can significantly slow down the application, negatively impacting the user experience.
So, how do we improve performance by rendering only the necessary pins?
A straightforward solution is to dynamically render only the points of interest within the user’s current viewport.
By calculating the coordinates of the visible area, we can filter out pins outside the view.
Even at high zoom levels—where clustering is less effective—this ensures that only relevant points are rendered, avoiding unnecessary performance overhead.
To retrieve the current map boundaries, check your mapping library’s documentation for built-in methods to get the visible coordinates.
This approach results in a scalable, high-performance interactive map, offering a smooth user experience without unnecessary rendering overhead.
By implementing clustering and viewport-based rendering, we can effectively manage large datasets while keeping our application fast and responsive.
RELATED
ARTICLES
Our technological stories to delve deeper into the world of software development: methods, approaches, and the latest technologies to generate value.