Fast rendering thousands of markers using Google Maps API

Keywords

maps, Google Maps API, dots, markers, javascript, API, geolocating

The Problem

We all love Google Maps API, don’t we? It has powerful and well-documented features, the map itself looks very human-readable and convenient. We can easily (just in few steps) insert Google Map block to our website, draw on it, insert dot that will show location of our office etc.

dot example

But once upon a time I’ve got list of postal addresses and I had to show all of them as dots on a map. There were thousands of them… And they had to be rendered fast, because this is the site homepage.

After several hours of investigation there was a plan:

  1. Get geolocation information (latitude and longitude) from each postal address.
  2. Store all this information in database.
  3. Retrieve this information once user is visiting this page.
  4. Create Marker instance for each dot
  5. Group them on a map into clusters

Sounds nice, but there was a problem. Let me describe each step first.

The Solution

Get geolocation information (latitude and longitude) from each postal address.

Google Maps Platform has submodule called Google Places. That is exactly what we’re going to use.

Using this GET Request:

https://maps.googleapis.com/maps/api/place/findplacefromtext/json?input=Museum%20of%20Contemporary%20Art%20Australia&inputtype=textquery&fields=geometry&key=YOUR_API_KEY

where YOUR_API_KEY is your API key:) we can get the next Response:

{
    "candidates" : [
        {
            "geometry" : {
                "location" : {
                    "lat" : -33.8599358,
                    "lng" : 151.2090295
                },
                "viewport" : {
                    "northeast" : {
                        "lat" : -33.85824767010727,
                        "lng" : 151.2102470798928
                    },
                    "southwest" : {
                        "lat" : -33.86094732989272,
                        "lng" : 151.2075474201073
                    }
                }
            }
        }
    ],
    "status" : "OK"
}

and then just save geometry field.

In order to prevent many requests at a time, Google doesn’t allow to get more then 10 requests per second. So, we need to grab longitude and latitude one time and store it to our database.

Store all this information in database.

We need to write small script and save all responses from Google to our database. I understand that in every system code will change, so I just write a pseudo-code:

for_each address__line:
let response = request_to_google_places(address_line)
if (response['status'] == 'OK')
    save_to_db(response['candidates']['geometry'])
else
    wait_for_1_sec

//call request again
let response_again = request_to_google_places(address_line)
if (response_again['status'] == 'OK')
    save_to_db(response_again['candidates']['geometry'])
else:
    //skip - not found
    wait_for_1_sec

wait_for_1_sec – is something line Thread.sleep(1000) in programming language that you use. I recommend to use language that has Threads implementation – just to have .sleep() method.

One more thing for you to know. We don’t need more than 5 decimal places after comma in longitude and latitude fields. I would delete these tails to save space and have less response size in future.

Retrieve this information once user is visiting this page.

After all coordinates are saved we need to build simple REST API call to get them back to our page:

get_coordinates_api:
let data = get_all_from_db()
return data.map(item => {lo: item.longitude, la: item.latitude})

In order to save even more for the Response body – change field names to the first symbols only.

Create Marker instance for each dot

Once we’ve created API for grabbing all our coordinates, we can do something like this on UI part

let all_coords = get_coordinates();
let dots = [];
for (let i = 1; i < all_coords.length; i++) {
    dots.push(new window.google.maps.Marker({
        position: {
            lat: +all_coords[i].la,
            lng: +all_coords[i].lo
        },
        clickable: false,
        icon: {
            path: window.google.maps.SymbolPath.CIRCLE,
            fillColor: '#263271',
            fillOpacity: 1,
            strokeColor: '#263271',
            strokeOpacity: 1,
            strokeWeight: 1,
            scale: 5
        }
    }));
}

You may notice that I’m not using forEach method from EcmaScript 5.1. That’s because good old for-loop is faster. But proof that it’s true can take one more blog post. Just trust me!

Group them on a map into clusters

So, we have our dots like this:)

dots

I hope you agree that this is not good..

Lets use Marker Clusterer for grouping them:

new MarkerClusterer(map, dots, {imagePath: 'assets/images/icons/m', zoomOnClick: false});

Now it’s better:

markers

You may think that this is all. But if you have 10,000 dots to render you will see that browser working really slow first several seconds while all markers is being rendered.

The Next Problem

We can’t get a second Thread using JavaScript to make page load faster and get rid of freezes. JavaScript has no threads at the language level. So, we’re stuck?

Maybe.

Again, several hours of investigating the problem and I came up with another plan:

  1. Measure every little piece of code to make sure where the bottleneck is.
  2. Fix the bottleneck, if possible.

Code benchmark (ugly but helpful way)

I had two suspects – for-loop that creates many markers and MarkerClusterer constructor.

Together I wrote something like this:

let all_coords = get_coordinates();
let dots = [];
let date1 = new Date().getTime();

for (let i = 1; i < all_coords.length; i++) {
    dots.push(new window.google.maps.Marker({
        position: {
            lat: +all_coords[i].la,
            lng: +all_coords[i].lo
        },
        clickable: false,
        icon: {
            path: window.google.maps.SymbolPath.CIRCLE,
            fillColor: '#263271',
            fillOpacity: 1,
            strokeColor: '#263271',
            strokeOpacity: 1,
            strokeWeight: 1,
            scale: 5
        }
    }));
}

let date2 = new Date().getTime();
console.log(date2 - date1);

new MarkerClusterer(map, dots, {
    imagePath: 'assets/images/icons/m',
    zoomOnClick: false
});

let date3 = new Date().getTime();
console.log(date3 - date2);

In console we will have two values – number of milliseconds for which code is executed.

benchmark

The for-loop takes us more than 9 seconds! But the MarkerClusterer call is fine.

Fix the bottleneck, if possible.

The bottleneck is found. It’s in the Marker constructor. What can we do?

We can call Marker constructor just one time and then copy the Marker instance for each dot. I’ve used Lodash _.cloneDeep method:

let dots = [];

dots.push(new window.google.maps.Marker({
    position: {
        lat: +addresses[0].la,
        lng: +addresses[0].lo
    },
    clickable: false,
    icon: {
        path: window.google.maps.SymbolPath.CIRCLE,
        fillColor: '#263271',
        fillOpacity: 1,
        strokeColor: '#263271',
        strokeOpacity: 1,
        strokeWeight: 1,
        scale: 5
    }
}));

for (let i = 1; i < addresses.length; i++) {
    const currentDot = cloneDeep(dots[0]);
    currentDot.position = new window.google.maps.LatLng(addresses[i].la, addresses[i].lo);
    dots.push(currentDot);
}

new MarkerClusterer(map, dots, {
    imagePath: 'assets/images/icons/m',
    zoomOnClick: false
});

this.markers = dots;

In this piece of code we create Marker only once and then just copy it for each coordinate. It gives us result of 612 millis:

benchmark_after

We have speeded up our code in more that 10 times!

Conclusion

Google Maps API is a great platform to do some cool stuff with maps and geolocating. Unfortunately, it isn’t so fast as I’ve expected. But upon further reflection we could achieve better performance.