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 © <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>
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