Lazy-loading Google Maps
I would call this year a year of web performance. The very useful web performance solutions developed by people who make websites. The cult of website performance analyzers. The rise of mobile data users. The releases of faster mobile browsers. The better raking on Google for faster websites. The conferences dedicated to web performance. All of these affected my work and web performance has become a core factor. I started questioning every element of a website: can I optimize this?
For my latest responsive freelance project there was a contact page where I had to display several Google Maps instances on a single page. I believe you can see where I am heading to: what if a user is not going to scroll down the page because the very first map is what a user was looking for? What if the user is accessing the page on a smartphone under the cellular internet connection?
The easiest way would be unconditionally loading a script file and map instances at once. But that would be a very very wrong way. The right way is to lazy-load the script file and map instances one by one.
jQuery plugin
As usual, I have developed a tiny jQuery plugin, which just loads the maps and then gives away the full control of the loaded ones to you:
- jquery.lazy-load-google-maps.js (uncompressed; 2 KB);
- jquery.lazy-load-google-maps.min.js (minified; 1 KB).
$(selector).lazyLoadGoogleMaps([options]);
The plugin accepts some options. The defaults are:
$(selector).lazyLoadGoogleMaps({
key: false,
// just an API key if you have one
libraries: false,
// libraries to load, i.e. 'geometry,places'
signed_in: false,
// sign-in on a map enabled/disabled
language: false,
// set language, i.e. 'en', 'en-GB'
region: false,
// set region, i.e. 'GB'
callback: false
/*
a callback function called within every map instance initiation;
there are two variables that are passed as parameters to the function:
function( container, map )
* container - the container element which has a selector 'selector' (see the first line)
* map - the map instance which is a part of Google Maps API
*/
});
It also has two helper methods, which I’ll explain later in the post:
var $instance = $(selector).lazyLoadGoogleMaps([options]);
$instance.debounce(duration, fn);
$instance.throttle(duration, fn);
How the plugin works?
On every scroll and browser resize interval the plugin checks whether there are any maps in the viewport to be displayed. If yes, it loads (if it wasn’t loaded before) Google Maps API script file and then initiates the corresponding map instances. Finally, there’s a callback function which is called within every map instance initiation so that you can continue doing whatever you need with your maps.
Let’s say you have five maps with different locations, and you need to mark them and center the view. The most simple practical use of the plugin would look like this:
<div class="google-map" data-lat="40.7056258" data-lng="-73.97968"></div>
<div class="google-map" data-lat="51.5072113" data-lng="-0.1144521"></div>
<div class="google-map" data-lat="31.2243489" data-lng="121.4767528"></div>
<div class="google-map" data-lat="48.8588589" data-lng="2.3470599"></div>
<div class="google-map" data-lat="35.7090711" data-lng="139.7321219"></div>
<script src="jquery.js"></script>
<script src="jquery.lazy-load-google-maps.min.js"></script>
<script>
;(function($, window, document, undefined) {
$('.google-map').lazyLoadGoogleMaps({
callback: function(container, map) {
var $container = $(container),
center= new google.maps.LatLng($container.attr('data-lat'), $container.attr('data-lng'));
map.setOptions({ zoom: 15, center: center });
new google.maps.Marker({ position: center, map: map });
}
});
})(jQuery, window, document);
</script>
The callback
function will be called 5 times here and will walk through the each map. Using HTML [data-*]
attributes to specify the coordinates and later retrieving them with jQuery’s $container.attr( 'data-*' )
makes the maps easily maintainable. You can pass more information in this manner according to your needs. So basically, you can fully use Google Maps API inside the callback function. You can also store the map instances in an array and use the API outside the callback function. This is the next example…
Responsive maps
Of course, your map containers are fluid because you design responsive websites. But the problem is that when a container resizes, the map looses focus. When the container becomes smaller, the location mark usually hides; when larger, you usually find the mark in the part of the container where you don’t want it to be. Google Maps API does not take care of this by default, so we have to.
Let’s assign the information of the default center for each map instance and, as mentioned previously, store the map instances in an array. Finally, during each browser resize interval, we walk through the map instances and set the map center to the default value.
var $window = $(window), // variable cache for a better performance
mapInstances = [], // stack of map instances
$pluginInstance = $('.google-map').lazyLoadGoogleMaps({
callback: function(container, map) {
var $container = $(container),
center = new google.maps.LatLng($container.attr('data-lat'), $container.attr('data-lng'));
$.data(map, 'center', center);
mapInstances.push(map);
}
});
$window.on('resize', $pluginInstance.debounce(1000, function() {
$.each(mapInstances, function() {
this.setCenter($.data(this, 'center'));
});
}));
Wondering what $pluginInstance.debounce()
does here? The next part of the post is exactly about the importance of throttling and deboucing events in JavaScript…
Throttling & debouncing
Have you ever experienced a browser crash when flipping your smartphone/tablet? There are a lot of chances it has happened because of the poorly developed website…
Throttling. Simply put, the number 250 (0.25 seconds) means that during the page scroll, the script makes 4 calculations per second at most. Without throttling, it would be ~20 times per second, but there’s totally no need to calculate it that often and, therefore, to put so much weight on user’s CPU.
Debouncing. The number 1000 (1 second) means that the calculation will only be performed 1 second after the last browser window resize action. For example, if you keep resizing the window for several seconds by moving the mouse around, no calculations will be performed during that time. The action is only performed after you release the mouse button and 1 second passes – the code then thinks that you have finished resizing the window. There’s totally no need for calculations during the resize process.
My advice: if you have never implemented throttling and debouncing before, you should check your codes and update where necessary. Be responsible.
Both throttling and debouncing are used inside the plugin; therefore, I 've made them available as plugin methods so you can use them in order to optimize your maps experience.
Efficiency
Finally, let’s see how we can save the mobile data plans of our visitors. The hypothetical situation I mentioned previously: a user opens the contact page on his smartphone connected to cellular network, sees the first map and does not scroll down the page because the first map is what was looking for. If the contact page was like in my demo, the whole download size would be 2.58 MB. Without lazy-load it could have been 6.44 MB. In this case we save 3.86 MB and the mental impact was that the page was loaded 2.5 times faster. Way to go!
The demo contains all of the features I described previously and there are a few user experience enriching additions.
You can also contribute, follow the project on GitHub.
I am going to share more lazy-loading techniques, so be sure to subscribe for the updates! Thank you for reading.
Changelog
- The plugin now is on GitHub too. Fixed the key issue: use
key
instead ofapi_key
. (2016-11-23) - Added new options:
libraries
,signed_in
,language
,region
. (2015-11-07)