React: come ottimizzare le performance di una mappa interattiva
Le mappe interattive sono strumenti potenti per migliorare l’esperienza utente, ma gestire grandi quantità di dati senza compromettere le performance può essere una sfida. In questo articolo esploriamo strategie efficaci per ottimizzare una mappa in React, riducendo il carico di rendering e migliorando la scalabilità.
Vito Russo
Frontend JuniorReact e mappe interattive
Negli ultimi anni, sempre più applicazioni e siti web hanno integrato funzionalità basate sulla visualizzazione di elementi su mappa: app per la compravendita di immobili, negozi, servizi di renting, food delivery e molto altro.
Consultare informazioni da un semplice elenco può risultare poco intuitivo e dispendioso in termini di tempo. A volte è necessario avere una visione d’insieme immediata, altre volte è fondamentale conoscere la posizione geografica di un risultato o la sua distanza da un punto di interesse.
Integrare una mappa nella propria applicazione web può migliorare notevolmente l’esperienza utente.
Ma come farlo in modo efficiente? Quali sfide comporta l’implementazione di una mappa interattiva in una web app?
In questo articolo vedremo alcuni accorgimenti utili per migliorare le performance.
L’ambiente di sviluppo
Prima di entrare nel dettaglio, è bene specificare che, pur concentrandomi sugli aspetti strategici e sulle ottimizzazioni, farò riferimento alle tecnologie utilizzate nel progetto: React, TypeScript, Redux e la libreria Pigeon Maps.
Troppi pin sulla mappa
In un’applicazione web che stavo sviluppando, avevamo l’esigenza di visualizzare una serie di punti di interesse su una mappa.
Ogni pin doveva:
- Mostrare un’icona rappresentativa del punto di interesse
- Fornire informazioni aggiuntive all’hover del mouse
Un esempio concreto sono le mappe di Airbnb o Immobiliare.it, dove gli annunci vengono mostrati con pin interattivi.
Il problema di performance
Se i punti di interesse sono pochi, la soluzione è semplice:
- Recuperare i dati dal backend
- Renderizzarli direttamente sulla mappa
Ma cosa accade se ci ritroviamo con migliaia o decine di migliaia di punti?
- Se più punti si trovano vicini, lo zoom out sulla mappa li farà sovrapporre?
- Quanto tempo servirà per renderizzare tutti i pin?
- Ha senso caricare pin anche per le aree che l’utente non sta visualizzando?
La soluzione: clustering e rendering intelligente
Per garantire una soluzione scalabile e performante, è utile adottare due strategie:
1. Clustering dei pin in base allo zoom
L’idea è semplice: raggruppare i pin vicini tra loro quando la mappa è a livelli di zoom bassi e mostrarli separatamente man mano che l’utente ingrandisce la visualizzazione.
Esempio pratico:
- Zoom 13 → I punti distanti meno di 100 km si aggregano in un unico pin
- Zoom 14 → Soglia ridotta a 50 km
- E così via, fino a visualizzare ogni punto separato ai livelli di zoom più alti
Vantaggi:
- Riduce il numero di elementi da renderizzare
- Evita la sovrapposizione di pin
- Mantiene la mappa leggibile anche con molti dati
Per implementarlo, ho utilizzato funzioni di utility per:
- Calcolare la distanza tra due punti
- Raggruppare i pin in base alle soglie di distanza
- Determinare il punto mediano per posizionare il pin aggregato
Ecco un esempio di codice:
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)
);
};
Se vuoi risparmiare tempo, puoi anche cercare librerie già pronte per il clustering, come Supercluster per Leaflet o Google Maps.
2. Renderizzare solo i pin visibili nella viewport
Un’altra problematica riguarda il rendering inefficiente di tutti i pin contemporaneamente. Alcune librerie di mappe, come Pigeon Maps, caricano l’intera mappa del mondo con tutti i punti di interesse, anche quelli non visibili all’utente.
Soluzione: renderizzare solo i pin che rientrano nella porzione di mappa visibile.
Per farlo, basta:
- Recuperare le coordinate della viewport attuale (gli angoli della mappa visibili all’utente)
- Filtrare i punti di interesse in base a tali coordinate
Risultato: l’app renderizza solo i pin effettivamente necessari, evitando inutili rallentamenti.
Per ottenere le coordinate visibili, consulta la documentazione della libreria di mappe che stai utilizzando.
Conclusione
Se la tua applicazione deve gestire una grande quantità di dati su mappa, implementare clustering e filtering è fondamentale per ottimizzare le performance.
- Clustering dei pin → Per evitare sovrapposizioni e ridurre il numero di elementi
- Rendering intelligente → Per mostrare solo i dati visibili nella viewport
Con questi accorgimenti, migliorerai la user experience e garantirai performance fluide, anche in applicazioni con migliaia di punti di interesse.
Hai già affrontato un problema simile? Condividi con noi la tua esperienza.
ARTICOLI
CORRELATI
I nostri racconti tecnologici per approfondire il mondo dello sviluppo software: metodi, approcci e ultime tecnologie per generare valore.