Tech Flow
03.02.2025

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à.

Scritto da:
Vito Russo

Vito Russo

Frontend Junior
Article cover image

SHARE

React 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.

Mappa

Il problema di performance

Se i punti di interesse sono pochi, la soluzione è semplice:

  1. Recuperare i dati dal backend
  2. 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:

  1. Recuperare le coordinate della viewport attuale (gli angoli della mappa visibili all’utente)
  2. 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.

GET IN
TOUCH

Il nostro lavoro è trasformare le tue esigenze in soluzioni. 

Contattaci per progettare insieme quella più adatta a te.