Speed up Google Maps(and everything else) with async & defer

It’s 2016 and if you’re not using unblocking async and defer tags on your client side Javascript, you need an intervention.

If the async attribute is present, then the script will be fetched in parallel to parsing and evaluated as soon as it is available (potentially before parsing completes).
If the async attribute is not present but the defer attribute is present, then the classic script will be fetched in parallel and evaluated when the page has finished parsing.
Source: WHATWG
HTML parsing, and async & differ fetch and execution of Javascript on Browsers

If neither attribute is present, then the script is fetched and evaluated immediately, blocking parsing until these are both complete, thereby, making everything slow and crappy.

One of the most obvious uses of this is in loading Google Maps. Do we need our DOM to wait to render, until Google Maps has been fetched and executed? The answer is mostly no.

  • So which tag do we use? defer or async? or both?
  • How and where do we write the map initialisation function?

A normal(blocking) Google Maps implementation would look something like this:

<script src="https://maps.googleapis.com/maps/api/js"></script>
...
<script>
function initMap() {
var map = new google.maps.Map(document.getElementById('map'), {
center: {lat: -34.397, lng: 150.644},
zoom: 8
});
}
google.maps.event.addDomListener(window, 'load', initMap);
</script>

We make two changes here:

Add async and defer to the script tag

<script src="https://maps.googleapis.com/maps/api/js" async defer></script>

We add both because async is not supported on all browsers, and defer serves as a fallback for these older browsers.

If async is present, then the script will be executed as soon as it is available, but without blocking further parsing of the page. If async is not present but defer is, then the script is executed when the page has finished parsing.

Trigger the initMap function on callback

<script>
function initMap() {
var map = new google.maps.Map(document.getElementById('map'), {
center: {lat: -34.397, lng: 150.644},
zoom: 8
});
}
</script>
<script src="https://maps.googleapis.com/maps/api/js?callback=initMap" async defer></script>

Note that the <script>’s src has changed to include the callback.

This allows the initMap function to be called immediately once the Maps script has been fetched.

The <script> tag has been moved below the initMap function to prevent the Maps API from calling it before it has been initialised.

Performance

Sync

Sync (source: https://googlegeodevelopers.blogspot.in/2015/09/map-tips-speeding-up-page-load-times.html)

DOMContentLoaded is triggered at ~600ms (658ms in the screenshot below) and map customizations can begin around 700–1000ms.

Async

Async (source: https://googlegeodevelopers.blogspot.in/2015/09/map-tips-speeding-up-page-load-times.html)

DOMContentLoaded is triggered at 35ms and map customizations can begin at around 300–500ms.

Gotchas

As the script tags are no longer synchronously loading in a predictable order, any code using the google.maps namespace needs to run within the callback or sometime afterwards.

To work around this constraint, you can either fall back to loading the API synchronously or mix in your code with the parent class once it’s available. If you choose to load it synchronously, you can still move the <script> tag to the end of the <body> section so your page renders quickly.

There are 2 particular cases you should look out for:

  • Subclassing anything in the Google Maps JavaScript API (for example, custom overlays). You must ensure your classes are defined after the callback has been executed.
  • LatLngs defined outside of the map initialization function. Most functions now accept LatLngLiterals, so you can simply pass { lat: 33, lng: 151 }.

Happy coding!