From 8eb60884ede1f6f492699b75e8851bad2bdf1e54 Mon Sep 17 00:00:00 2001 From: karilint Date: Fri, 29 May 2026 14:10:44 +0300 Subject: [PATCH 1/3] fix: Lazy load country polygons to prevent UI freeze (#1056) Fix #1056: Web freezing when loading locality map. The countryPolygons.ts file is 4.4MB and was being imported synchronously, causing the main thread to block and the UI to freeze. Solution: - Dynamically import countryPolygons using async import() - Only load the data after the map component mounts - Add loading state for country borders This prevents the large module from blocking the main thread during initial page load. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- frontend/src/components/Map/LocalitiesMap.tsx | 81 ++++++++++++------- 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/frontend/src/components/Map/LocalitiesMap.tsx b/frontend/src/components/Map/LocalitiesMap.tsx index 8a50c5058..a4fbb63c1 100755 --- a/frontend/src/components/Map/LocalitiesMap.tsx +++ b/frontend/src/components/Map/LocalitiesMap.tsx @@ -1,7 +1,6 @@ import { useGetLocalityDetailsQuery } from '../../redux/localityReducer' -import { useState, useEffect, useRef } from 'react' +import { useState, useEffect, useRef, useCallback } from 'react' import 'leaflet/dist/leaflet.css' -import { countryPolygons } from '../../country_data/countryPolygons.ts' import L, { LatLngExpression } from 'leaflet' import { SimplifiedLocality } from '../../shared/types/data.js' import { skipToken } from '@reduxjs/toolkit/query' @@ -34,6 +33,51 @@ export const LocalitiesMap = ({ localities, isFetching }: Props) => { selectedLocality ?? skipToken ) + // Lazy load country polygons to avoid blocking the main thread with 4.4MB of data + const [countryPolygons, setCountryPolygons] = useState(null) + + useEffect(() => { + // Dynamically import country polygons to avoid blocking on initial load + void import('../../country_data/countryPolygons.ts').then(module => { + setCountryPolygons(module.countryPolygons) + }) + }, []) + + const addCountryBorders = useCallback( + (mapInstance: L.Map) => { + if (!countryPolygons) return + + // Create a polygon layer for the country borders that is in layer control panel. + const borderLayer = L.layerGroup() + countryPolygons.forEach(countryBorder => { + L.polygon(countryBorder as LatLngExpression[], { color: 'gray', weight: 1 }).addTo(mapInstance) + + const polygon = L.polygon(countryBorder as LatLngExpression[], { + color: '#136f94', + fillOpacity: 0.3, + weight: 1, + }) + borderLayer.addLayer(polygon) + }) + + const baseMaps = { + OpenTopoMap: L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { + attribution: 'Map data: © OpenTopoMap (CC-BY-SA)', + noWrap: true, + }).addTo(mapInstance), + OpenStreetMap: L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', + noWrap: true, + }), + Countries: borderLayer, + } + + // Layer control + L.control.layers(baseMaps, {}, { position: 'topright' }).addTo(mapInstance) + }, + [countryPolygons] + ) + useEffect(() => { if (!mapRef.current) return @@ -46,44 +90,20 @@ export const LocalitiesMap = ({ localities, isFetching }: Props) => { // ---- Base maps ---- // OpenTopoMap - const topomap = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { + L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { attribution: 'Map data: © OpenTopoMap (CC-BY-SA)', noWrap: true, }).addTo(mapInstance) // OpenStreetMap - const osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', noWrap: true, }) - // ---- Layers on top of base maps ---- - - // Create a polygon layer for the country borders that is in layer control panel. - const borderLayer = L.layerGroup() - countryPolygons.forEach(countryBorder => { - L.polygon(countryBorder as LatLngExpression[], { color: 'gray', weight: 1 }).addTo(mapInstance) - - const polygon = L.polygon(countryBorder as LatLngExpression[], { - color: '#136f94', - fillOpacity: 0.3, - weight: 1, - }) - borderLayer.addLayer(polygon) - }) - - const baseMaps = { - OpenTopoMap: topomap, - OpenStreetMap: osm, - Countries: borderLayer, - } - // Scale bar L.control.scale({ position: 'bottomright' }).addTo(mapInstance) - // Layer control - L.control.layers(baseMaps, {}, { position: 'topright' }).addTo(mapInstance) - // North-arrow const northArrowControl = L.Control.extend({ onAdd: function () { @@ -96,10 +116,13 @@ export const LocalitiesMap = ({ localities, isFetching }: Props) => { const northArrow = new northArrowControl({ position: 'bottomright' }) northArrow.addTo(mapInstance) + // Add country borders once they're loaded + addCountryBorders(mapInstance) + return () => { mapInstance.remove() } - }, []) + }, [addCountryBorders]) useEffect(() => { if (!map || isFetching) return From 694181623b752ec676dd6a52d0e5a2e47243616b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 11:55:00 +0000 Subject: [PATCH 2/3] fix: avoid map reinit during lazy country border loading --- frontend/src/components/Map/LocalitiesMap.tsx | 91 ++++++++++--------- 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/frontend/src/components/Map/LocalitiesMap.tsx b/frontend/src/components/Map/LocalitiesMap.tsx index a4fbb63c1..d1031dd16 100755 --- a/frontend/src/components/Map/LocalitiesMap.tsx +++ b/frontend/src/components/Map/LocalitiesMap.tsx @@ -1,5 +1,5 @@ import { useGetLocalityDetailsQuery } from '../../redux/localityReducer' -import { useState, useEffect, useRef, useCallback } from 'react' +import { useState, useEffect, useRef } from 'react' import 'leaflet/dist/leaflet.css' import L, { LatLngExpression } from 'leaflet' import { SimplifiedLocality } from '../../shared/types/data.js' @@ -24,6 +24,7 @@ export const LocalitiesMap = ({ localities, isFetching }: Props) => { const [selectedLocality, setSelectedLocality] = useState(null) const mapRef = useRef(null) const [map, setMap] = useState(null) + const borderLayerRef = useRef(null) const [isOpen, setIsOpen] = useState(false) const [localityDetailsIsOpen, setLocalityDetailsIsOpen] = useState(false) const [clusteringEnabled, setClusteringEnabled] = useState(true) @@ -37,46 +38,21 @@ export const LocalitiesMap = ({ localities, isFetching }: Props) => { const [countryPolygons, setCountryPolygons] = useState(null) useEffect(() => { - // Dynamically import country polygons to avoid blocking on initial load - void import('../../country_data/countryPolygons.ts').then(module => { - setCountryPolygons(module.countryPolygons) - }) - }, []) - - const addCountryBorders = useCallback( - (mapInstance: L.Map) => { - if (!countryPolygons) return + let mounted = true - // Create a polygon layer for the country borders that is in layer control panel. - const borderLayer = L.layerGroup() - countryPolygons.forEach(countryBorder => { - L.polygon(countryBorder as LatLngExpression[], { color: 'gray', weight: 1 }).addTo(mapInstance) - - const polygon = L.polygon(countryBorder as LatLngExpression[], { - color: '#136f94', - fillOpacity: 0.3, - weight: 1, - }) - borderLayer.addLayer(polygon) + // Dynamically import country polygons to avoid blocking on initial load + void import('../../country_data/countryPolygons.ts') + .then(module => { + if (mounted) setCountryPolygons(module.countryPolygons) + }) + .catch(() => { + if (mounted) setCountryPolygons([]) }) - const baseMaps = { - OpenTopoMap: L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { - attribution: 'Map data: © OpenTopoMap (CC-BY-SA)', - noWrap: true, - }).addTo(mapInstance), - OpenStreetMap: L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors', - noWrap: true, - }), - Countries: borderLayer, - } - - // Layer control - L.control.layers(baseMaps, {}, { position: 'topright' }).addTo(mapInstance) - }, - [countryPolygons] - ) + return () => { + mounted = false + } + }, []) useEffect(() => { if (!mapRef.current) return @@ -90,20 +66,32 @@ export const LocalitiesMap = ({ localities, isFetching }: Props) => { // ---- Base maps ---- // OpenTopoMap - L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { + const topomap = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { attribution: 'Map data: © OpenTopoMap (CC-BY-SA)', noWrap: true, }).addTo(mapInstance) // OpenStreetMap - L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + const osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', noWrap: true, }) + // Create a polygon layer for the country borders that is in layer control panel. + const borderLayer = L.layerGroup() + borderLayerRef.current = borderLayer + const baseMaps = { + OpenTopoMap: topomap, + OpenStreetMap: osm, + Countries: borderLayer, + } + // Scale bar L.control.scale({ position: 'bottomright' }).addTo(mapInstance) + // Layer control + L.control.layers(baseMaps, {}, { position: 'topright' }).addTo(mapInstance) + // North-arrow const northArrowControl = L.Control.extend({ onAdd: function () { @@ -116,13 +104,28 @@ export const LocalitiesMap = ({ localities, isFetching }: Props) => { const northArrow = new northArrowControl({ position: 'bottomright' }) northArrow.addTo(mapInstance) - // Add country borders once they're loaded - addCountryBorders(mapInstance) - return () => { + borderLayerRef.current = null mapInstance.remove() } - }, [addCountryBorders]) + }, []) + + useEffect(() => { + if (!map || !countryPolygons || !borderLayerRef.current) return + + if (borderLayerRef.current.getLayers().length > 0) return + + countryPolygons.forEach(countryBorder => { + L.polygon(countryBorder as LatLngExpression[], { color: 'gray', weight: 1 }).addTo(map) + + const polygon = L.polygon(countryBorder as LatLngExpression[], { + color: '#136f94', + fillOpacity: 0.3, + weight: 1, + }) + borderLayerRef.current?.addLayer(polygon) + }) + }, [countryPolygons, map]) useEffect(() => { if (!map || isFetching) return From 5450e511bb2b60319b8bf40084f4146fd1f0dddd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 12:00:42 +0000 Subject: [PATCH 3/3] fix: avoid duplicate country border layers --- frontend/src/components/Map/LocalitiesMap.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/components/Map/LocalitiesMap.tsx b/frontend/src/components/Map/LocalitiesMap.tsx index d1031dd16..cf2e3427f 100755 --- a/frontend/src/components/Map/LocalitiesMap.tsx +++ b/frontend/src/components/Map/LocalitiesMap.tsx @@ -113,11 +113,9 @@ export const LocalitiesMap = ({ localities, isFetching }: Props) => { useEffect(() => { if (!map || !countryPolygons || !borderLayerRef.current) return - if (borderLayerRef.current.getLayers().length > 0) return + borderLayerRef.current.clearLayers() countryPolygons.forEach(countryBorder => { - L.polygon(countryBorder as LatLngExpression[], { color: 'gray', weight: 1 }).addTo(map) - const polygon = L.polygon(countryBorder as LatLngExpression[], { color: '#136f94', fillOpacity: 0.3,