1
\$\begingroup\$

I need to show a map of the United States using Leaflet, where if the user clicks on a state, the map zooms in to view all of the state's census tracts. I need to do this without a significant delay in loading.

The best approach I have tried so far requests all of the census tract data when the page loads. I send these requests using a WebWorker, which receives all of the data in one message and then fires off 51 separate XHR requests. As the WebWorker receives each XHR response, it parses the response's JSON and sends this back to the main thread. As the main thread receives each parsed JSON, it creates a GeoJSON Leaflet layer for the state, appending it to a collection for future reference.

There is a significant, unacceptable delay when the page first loads and weird lags later on. When I check the memory usage in Firefox, it looks like the page is using 900+ MB. If I take the census tracts out of the code, the page uses much less memory.

How do I get rid of this delay/lag? I also think there is a memory leak because if I refresh the page a few times, the tab freezes and I have to close it.

I have also tried an alternative approach where I fire off an XHR request only when the user clicks on the state. However, there is a lag in receiving the response, leaving the user hanging for a couple seconds. And once all of the states have loaded, there is still the same overall lag in map movement.

The census tract data is 161.3 MB. The state data that I use at the beginning is 523 KB.

Full HTML, SSCCE:

<head>
    <style>
        body {
            padding: 0;
            margin: 0;
        }

        #map {
            height: 100vh;
            width: 100vw;
        }
    </style>

    <!-- Leaflet -->
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""/>
    <script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js" integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew==" crossorigin=""></script>

    <!-- my code -->
    <script src="test_script.js"></script>
</head>
<body>
    <div id="map"></div>
</body>

Full JS, SSCCE:

window.onload = function() {

    /*------------------------------------------------
    -------------- Initialize variables --------------
    ------------------------------------------------*/
    var mapboxAccessToken = 'pk.eyJ1IjoiYnJpZW5uYWtoIiwiYSI6ImNqbXRsNjN0aTAxZXAzbG13bmh0dGFjNm0ifQ.98TAXgq4Rg1LpM2Oq1rlWw';

    // Get main data
    var xmlhttp = new XMLHttpRequest();
    var url = 'http://briennakh.me/accessibility-maps/data/us_states_edited.json'
    xmlhttp.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            statesData = JSON.parse(this.responseText);
        }
    }
    xmlhttp.open('GET', url, false);
    xmlhttp.send();

    /*------------------------------------------------
    -------------- Create map ------------------------
    ------------------------------------------------*/

    // Create basemap 
    var basemap = L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
        id: 'mapbox/light-v9',
        attribution: '',
        tileSize: 512,
        zoomOffset: -1,
        maxZoom: 18,
        accessToken: mapboxAccessToken,
    });

    // Create map 
    var map = L.map('map', {
        center: new L.LatLng(37.8, -96),
        zoom: 4,
        layers: [basemap],
    });

    var stateLayer = L.geoJson(statesData, {
        style: stateStyle,
        onEachFeature: onEachState
    }).addTo(map);

    var censusTractLayers = {};

        if (typeof(Worker) !== "undefined") {
        w = new Worker('worker.js'); // to test worker, must serve on localhost, otherwise Security Error

        // Respond to message sent back from worker
        w.onmessage = function(e) {
            var state = e.data;
            console.log('Received message from worker for ' + state.name)
            censusTractLayers[state.name] = L.geoJson(state, {
                style: tractStyle,
                onEachFeature: onEachTract
            });
        }
    } else {
        // will need to inform user they need an updated browser
        console.log('No support for web worker.');
    } 

    // Load each state's census tracts in the background
    w.postMessage(statesData.features);

    /*------------------------------------------------
    -------------- Define styles ---------------------
    ------------------------------------------------*/
    function tractStyle(feature) {
        return {
            fillColor: 'grey',
            weight: .5, // no border
            opacity: 1,
            color: 'white',
            dashArray: '1',
            fillOpacity: 0.7
        }
    }

    function stateStyle(feature) {
        return {
            fillColor: 'grey',
            weight: 2,
            opacity: 1,
            color: 'white',
            dashArray: '3',
            fillOpacity: 0.7
        };
    }

    /*------------------------------------------------
    -------------- Define event handlers -------------
    ------------------------------------------------*/
    function onEachState(feature, layer) {
        layer.on({
            mouseover: function(e) {
                highlightFeature(e);
            },
            mouseout: function(e) {
                var feature = e.target;
                stateLayer.resetStyle(feature);
            },
            click: function(e) {
                map.fitBounds(e.target.getBounds());

                // Add census tract layer to map if it has already been loaded
                if (feature.properties.name in censusTractLayers) {
                    censusTractLayers[feature.properties.name].addTo(map);
                } else {
                    console.log('Not loaded yet');  
                }
            }
        });
    }

    function onEachTract(feature, layer) {
        layer.on({
            mouseover: function(e) {
                highlightFeature(e);
            },
            mouseout: function(e) {
                var feature = e.target;

                for (i in censusTractLayers) {
                    if(censusTractLayers[i].hasLayer(feature)) {
                        censusTractLayers[i].resetStyle(feature);
                    }
                }
            }
        });
    }

    function highlightFeature(e) {
        var feature = e.target;

        feature.setStyle({
            weight: 5,
            color: '#666',
            dashArray: '',
            fillOpacity: 0.7
        });

        if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
            feature.bringToFront(); // to prevent feature overlaps  
        }
    }
}

JS for web worker:

onmessage = function(e) {
    console.log('Message received from main script.');

    for (var i in e.data) {
        state = e.data[i].properties.name;

        var xmlhttp = new XMLHttpRequest();
        var url = 'http://briennakh.me/accessibility-maps/data/census tracts/geojsons/' + state + '.json'
        xmlhttp.onreadystatechange = function() {
            if (this.readyState == 4 && this.status == 200) {
                state_json = JSON.parse(this.responseText);
                postMessage(state_json);
            }
        }

        xmlhttp.open('GET', url);
        xmlhttp.send();
    }
}
```
\$\endgroup\$
1
  • \$\begingroup\$ You're using a synchronous XHR by specifying false in xmlhttp.open. Don't do it. Rework your code to use the proper modern asynchronous mode of XHR. \$\endgroup\$ – wOxxOm Mar 21 '20 at 6:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.