Search code examples
leafletleaflet.markercluster

Can you optimize the performance of my Leaflet MarkerCluster project code?


Can you detect some way to optimize the way I load and cluster my geojson data?

I need to load +10000 markers divided into subgroups but its very laggy when I am showing all markers. Its mostly when I zoom out that the performance is getting bad.

Here is a code example of loading two subgroups of markers when clicking at buttons on the map. In my project I have a lot more groups and markers, but the its the same structure as shown here.

// initialize the map
        var map = L.map('map', {maxZoom: 18}).setView([56.2, 9.8], 8);

        var osm = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {attribution: "Map data &copy; <a href='https://www.openstreetmap.org/'>OpenStreetMap</a> contributors", icon: "/ikoner/sat-layer.svg"}).addTo(map);;
        

        var mcg = L.markerClusterGroup({
          showCoverageOnHover: false,
          removeOutsideVisibleBounds: true,
          spiderfyOnMaxZoom: false,
          disableClusteringAtZoom: 15,
          chunkedLoading: true
        });

        var markerGroups = {
          markers1: L.featureGroup.subGroup(mcg),
          markers2: L.featureGroup.subGroup(mcg),
        };
  
        mcg.addTo(map);
        
        var markers1 = "https://gist.githubusercontent.com/FrederikPetri/95e559bb3a75eaf56a45b47bfe87630a/raw/gistfile1.txt";
        var markers2 = "https://gist.githubusercontent.com/FrederikPetri/4c00527dbdd8c46a7b6f6ecb7dc6c9d3/raw/gistfile1.json";
        
        var icon1 = "https://www.svgrepo.com/show/476893/marker.svg";
        var icon2 = "https://www.svgrepo.com/show/314953/place-marker.svg";
        
        loadGeoJSON(markers1, icon1, "markers1");
        loadGeoJSON(markers2, icon2, "markers2");


        function loadGeoJSON(url, iconUrl, group) {
          fetch(url)
            .then((response) => response.json())
            .then((data) => {
              data.features.forEach((feature) => {
                var marker = L.marker(
                  [feature.geometry.coordinates[1], feature.geometry.coordinates[0]],
                  { icon: L.icon({
                      iconUrl: iconUrl,
                      iconSize: [30, 30],
                    })
                  }
                );
                marker.bindPopup(feature.properties.beskrivels);
                marker.addTo(markerGroups[group]);
              });
            });
        }

        function filterClicked(id) {
          var group = markerGroups[id];
          if (map.hasLayer(group)) {
            map.removeLayer(group);
          } else {
            map.addLayer(group);
          }
          var element = document.getElementById(id);
          if (element.classList.contains("active")) {
            element.classList.remove("active");
          } else {
            element.classList.add("active");
          }
          
        }

        function showFilters() {
          var filterDiv = document.getElementById('filter-div');
          if (filterDiv.classList.contains("active")) {
            filterDiv.classList.remove("active");
          } else {
            filterDiv.classList.add("active");
          }
        }

        document.addEventListener('mouseup', function(e) {
          var filterDiv = document.getElementById('filter-div');
          if (!filterDiv.contains(e.target)) {
            if (filterDiv.classList.contains("active")) {
              filterDiv.classList.remove("active");
            }
          }
        });
body {
    padding: 0;
    margin: 0;
}
html, body, #map {
    height: 100%;
    width: 100vw;
}
:root {
  --blue: #0988f7;
}
h1, h2, h3, h4, p, label {
  font-family: Roboto, Arial, sans-serif;
}

.located-animation {
  width: 17px;
  height: 17px;
  border: 1px solid #fff;
  border-radius: 50%;
  background: var(--blue);
  animation: border-pulse 2s infinite;
}
@keyframes border-pulse {
  0% {
    box-shadow: 0 0 0 0 rgba(255, 255, 255, 1);
  }
  70% {
    box-shadow: 0 0 0 10px rgba(255, 0, 0, 0);
  }
  100% {
    box-shadow: 0 0 0 0 rgba(255, 0, 0, 0);
  }
}
.locate-active span {
  height: 8px;
  width: 8px;
  background-color: var(--blue);
  border-radius: 50%;
  position: absolute;
}

.locate-active img {
  filter: invert(37%) sepia(57%) saturate(2640%) hue-rotate(190deg) brightness(99%) contrast(96%);
  -webkit-filter: invert(37%) sepia(57%) saturate(2640%) hue-rotate(190deg) brightness(99%) contrast(96%);
}
.locate-button {
  position: absolute;
  top: 80px;
  left: 10px;
  width: 26px;
  height: 26px;
  z-index: 999;
  cursor: pointer;
  display: none;
  background: #fff;
  border: none;
  border-radius: 4px;
  box-shadow: 0 1px 5px rgb(0 0 0 / 65%);
}
.locate-button {
  width: 50px;
  height: 50px;
}
.leaflet-control-layers {
    background: transparent !important;
    border: none !important;
}
.leaflet-control-layers-toggle:after{ 
    content:"your text"; 
    color:#000 ;
}
.leaflet-control-layers-toggle{ 
    width:auto;
    background-position:3px 50% ;
    padding:3px;
    padding-left:36px;
    text-decoration:none;
    line-height:36px;
}
.custom-control-button {
  cursor: pointer;
  border: 2px solid rgba(0,0,0,0.2);
  border-radius: 50px;
  width: 50px;
  height: 50px;
  background: white;
  display: flex;
  align-items: center;
  justify-content: center;
}
.custom-topleft {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 999;
}
.custom-control-button {
  width: 45px;
  height: 45px;
  margin: 8px;
}
#filter-div, #layer-div {
  border-radius: 8px;
  bottom: 0;
  color: #3c4043;
  left: 0;
  max-height: 75%;
  overflow-y: auto;
  position: absolute;
  padding: 16px 24px 20px;
  -webkit-transition: left .2s cubic-bezier(0,0,.2,1);
  transition: left .2s cubic-bezier(0,0,.2,1);
  width: 220px;
  z-index: 1;
  margin: 15px;
  background: #fff;
  border: 0;
  box-shadow: 0 1px 3px rgba(60,64,67,0.3), 0 4px 8px 3px rgba(60,64,67,0.15);
  z-index: 999;
  display: none;
}
#filter-div.active, #layer-div.active {
  display: block;
}

.filter-ul {
  display: -webkit-box;
  display: -webkit-flex;
  display: flex;
  -webkit-box-orient: horizontal;
  -webkit-box-direction: normal;
  -webkit-flex-direction: row;
  flex-direction: row;
  -webkit-flex-wrap: wrap;
  flex-wrap: wrap;
  -webkit-box-pack: justify;
  -webkit-justify-content: space-between;
  justify-content: space-between;
  background: transparent;
  border: 0;
  border-radius: 0;
  font: inherit;
  list-style: none;
  margin: 0;
  outline: 0;
  overflow: visible;
  padding: 0;
  vertical-align: baseline;
  margin-left: -12px;
  margin-right: -12px;
}
.filter-ul.layer {
  justify-content: space-evenly !important
}
.filter-li {
  margin-bottom: 16px;
  margin-top: 4px;
  list-style: none;
}
.filter-button, .layer-button {
  color: inherit;
  cursor: pointer;
  position: relative;
  display: inline-block;
  height: 68px;
  line-height: 0;
  text-align: center;
  width: 72px;
  background: transparent;
  border: 0;
  border-radius: 0;
  font: inherit;
  list-style: none;
  margin: 0;
  outline: 0;
  overflow: visible;
  padding: 0;
  vertical-align: baseline;
}
.filter-icon, .layer-icon {
  display: inline-block;
  height: 48px;
  width: 48px;
}
.filter-icon {
  opacity: 0.2;
}
.filter-button.active img, .layer-button.active img  {
  opacity: 1;
  background-clip: content-box;
  border: 2px solid #ffffff;
  border-radius: 8px;
  width: 44px;
  height: 44px;
  outline: 2px solid #1a73e8;
}
.filter-button.active label, .layer-button.active label  {
  color: var(--blue);
}
.filter-label {
  font-weight: 400;
  color: #70757a;
  cursor: pointer;
  display: block;
  font-size: 12px;
  line-height: 16px;
  margin-top: 4px;
}
.filter-header {
  margin-bottom: 15px;
}
.filter-title {
  display: inline-block;
  color: #3c4043;
  font-weight: 500;
  font-size: 16px;
  margin: 0px;
}
.filter-close {
  background: none;
  border: none;
  cursor: pointer;
  float: right;
  height: 20px;
  width: 20px;
}
.filter-close-icon {
  width: 20px;
  height: 20px;
}
.filter-close-icon:hover {
  filter: invert(37%) sepia(57%) saturate(2640%) hue-rotate(190deg) brightness(99%) contrast(96%);
  -webkit-filter: invert(37%) sepia(57%) saturate(2640%) hue-rotate(190deg) brightness(99%) contrast(96%);
}
.custom-bottomleft {
  position: absolute;
  bottom: 0;
  left: 0;
  z-index: 999;
}
.filter-toggle {
  position: relative;
  text-align: center;
  margin: 15px;
  border: 2px solid #fff;
  border-radius: 7px;
  box-shadow: 0 1px 2px rgba(60,64,67,0.3), 0 1px 3px 1px rgba(60,64,67,0.15);
  background-color: #1B5A41;
  width: 65px;
  height: 65px;
  cursor: pointer;
}
.filter-toggle:hover {
  border: 4px solid #fff;
  margin: 13px;
}
.filter-toggle-icon {
  width: 100%;
}
.filter-toggle-text {
  position: absolute;
  bottom: 5px;
  left: 50%;
  transform: translate(-50%, 0%);
  margin: 0px;
  font-size: 11px;
  color: #ffffff;
}
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
    <meta charset="UTF-8">
    <title></title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
    <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
    <script src="https://unpkg.com/[email protected]/dist/leaflet.markercluster.js"></script>
    <script src="https://unpkg.com/[email protected]/dist/leaflet.featuregroup.subgroup.js"></script>

    <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"/>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.5.3/MarkerCluster.Default.css"/>


    <link rel="preconnect" href="https://fonts.gstatic.com"> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700;900&display=swap" rel="stylesheet">

</head>
<body>

    <div id="map"></div>

    <div class="custom-bottomleft">
      <div style="display: flex;">
        <div class="filter-toggle" onClick="showFilters()">
          <img class="filter-toggle-icon" style="width: 45px;" src="https://www.svgrepo.com/show/476890/map-marker.svg">
          <p class="filter-toggle-text">Markers</p>
        </div>
      </div>
    </div>
    
    <div id="filter-div">
      <header class="filter-header">
        <h2 class="filter-title">Markers</h2>
        <button class="filter-close" onClick="showFilters()">
          <img class="filter-close-icon" src="https://www.svgrepo.com/show/510924/close-md.svg">
        </button>
      </header>
      <ul class="filter-ul">
        <li class="filter-li">
          <button class="filter-button" id="markers1" onClick="filterClicked(this.id)">
            <img class="filter-icon" src="https://www.svgrepo.com/show/476893/marker.svg">
            <label class="filter-label">Markers1</label>
          </button>
        </li>
        <li class="filter-li">
          <button class="filter-button" id="markers2" onClick="filterClicked(this.id)">
            <img class="filter-icon" src="https://www.svgrepo.com/show/314953/place-marker.svg">
            <label class="filter-label">Markers2</label>
          </button>
        </li>
      </ul>
    </div>
 
</body>
</html>


Solution

  • Try loading the layers as geoJson instead of parsing them and loading individually.

    Also try creating the icons outside your function instead of dynamically creating them inside a loop.

    const icon1 = L.icon({
        iconUrl: "https://www.svgrepo.com/show/476893/marker.svg",
        iconSize: [30, 30],
    })
    
    const icon2 = L.icon({
        ...
    })
    
    function loadGeoJSON(url, icon, group) {
        fetch(url)
            .then((response) => response.json())
            .then((data) => {
                var geoJsonLayer = L.geoJson(data, {
                    onEachFeature: function (feature, layer) {
                        layer.bindPopup(feature.properties.beskrivels);
                    },
                    pointToLayer:  function(feature, latlng) {
                        return L.marker(latlng, { icon });
                    }
                });
                markerGroups[group].addLayer(geoJsonLayer)
            })
    }
    

    You can also use group.clearLayers(); instead of map.removeLayer(group); to improve performance. Link with the performance comparison: https://dev.to/agakadela/rendering-leaflet-clusters-fast-and-dynamically-let-s-compare-3-methods-291p