HTML5 History API
by Daniel Menjívar • #codeI recently updated my music site – DanielMenjivar.com – and apart from the complete design overhaul, one of the other major changes (that came about as a result of my quest for efficiency and speed), was the addition of a lot more JavaScript, or specifically, Ajax.
If you want, you can skip ahead to the sample code below…
Some History
The previous version of my music site (which went live ) was already using HTML5 markup and was using HTML5 Audio & Video (with jQuery) for all the media samples. But the site didn’t use very much JavaScript and it required a lot of page loads to access all the content. While it was great (for SEO and for the end-user) to have this content logically organized into separate (sub)pages, viewing the content was an inefficient process and perhaps also a little annoying. This was something I wanted to improve with the new version of the site.
Prior to July 2010, the even older version of my site didn’t have a sub-navigation at all and each main section of the site was huge. So for example, the charts section had what is now the /charts, /charts/samples, /charts/list and /charts/faqs content all on one page. I definitely didn’t want to go back to that, but I still needed to improve the experience of navigating from one sub-page to another within the same section of the site.
I knew that using AJAX to load content would improve this aspect of the user-experience and everything would feel much faster. Fast and efficient is good. But I didn’t like the idea of using #hashes since I wanted to keep my existing URLs intact. I wanted to make sure that visitors to my site could bookmark and share the original URL for the page without any appended #hashes. This was perfect for HTML5’s History API.
Getting Started with HTML5’s History API
My research into HTML5’s History API quickly revealed that it was surprisingly much less complicated than I anticipated. If you haven’t already read it, I strongly recommend reading through the History API article on DiveIntoHTML5.org. This is a great, well written article/tutorial that will help you get started fast.
The Popstate Event
The only hiccup I encountered was after the site went live, when I updated my MacBook Pro from Snow Leopard to Lion. Using the developer tools in the new version of Safari in Lion, I noticed that each initial page request was resulting in two requests being made – the original/full-body request and then an AJAX request for the same page. (I had also noticed this behavior in Chrome before the site went live, but I thought it was only on the search page so I implemented a server-side work-around for just that page – I guess I hadn’t tested it well enough!)
It took a lot of research and testing to find out what the problem was (it worked perfectly well in Firefox and in Safari before upgrading) and I eventually stumbled upon the answer – it had to do with the variances in how different browsers treat the popstate
event. The problem is that newer WebKit browsers fire the popstate
event on page load, whereas older WebKit browsers and newer Mozilla browsers don’t. So my popstate
event handler (which was supposed to be activated only when the user hits the back or forward buttons) was also firing on page load causing the original content to be replaced with AJAX content – which was the exact same content, just redundant… Not so efficient.
It didn’t take me long to find many suggestions on how to work around this but I didn’t find any that made much practical sense. Either they suggested manually firing the popstate
event on page load, or the proposed solutions messed up (or broke) the browsing history in some way or another. What I wanted was for my popstate
event handler to only fire when the user hits the back/forward buttons and to ignore the initial popstate
event if indeed the browser does fire it on page load. And not all browsers handle this the same way…
HTML5 History API Code Sample
Here’s a truncated excerpt of the JavaScript (with jQuery) that I’m currently using on DanielMenjivar.com with a solution to this popstate
event issue:
$(document).ready(function() {
// check to see if the browser supports the HTML5 history API
if (window.history && history.pushState)
{
var first_load = true;
// ! Open our sub-navigation and breadcrumb links using the history API
$('#nav-sub li a, #nav-breadcrumbs a').live('click', function(event) {
event.preventDefault();
// use the load_content function to load our main content, update classes, etc.
var page = $(this).attr('href');
load_content(page);
// update our address bar
history.pushState(null,null,page);
// and change the fact that this is not the original page load
first_load = false;
});
// ! Update our content when the user hits the back/forward button
$(window).bind('popstate', function(event) {
// only do something if it’s not the first page load
if (! first_load)
{
// use the load_content function to load our main content, update classes, etc.
load_content(location.pathname);
}
});
}
});
Almost every line is commented so it should be very easy to follow. On page load, ($(document).ready(function(){
), I’m setting the first_load
variable to true
. If/when the popstate
event fires on page load, nothing happens. When the user clicks on a link in the sub-navigation or breadcrumb trail, the content is loaded and the address bar is updated using the History API and then first_load
is set to false
, effectively allowing the popstate
event handler to run from that point on (if/when the user clicks the back/forward button). Make sense?
Fallback
Given that I wanted to keep my existing URLs intact and didn’t like the idea of using #hashes, (like I mentioned above), I didn’t create any fallback for this. So if the browser doesn’t support the History API, clicking those links will just result in a full page load rather than loading the partial content via AJAX.
When to Use the History API
It didn’t make sense to me that all pages/sections should use the History API, so I only implemented it on the sub-navigation and breadcrumb links – clicking on a page in the main navigation still requires a full page load. But most visitors to my site (not all, but most) only stay on one section of the site anyways and they rarely visit other sections (people interested in my charts don’t really care about my events, and people interested in seeing me play don’t care about ordering charts…) so it means that for most visits to the site, the majority of my content is loaded via AJAX. Quick and efficient.
At the same time, there is a lot of content on the site that I’m loading via AJAX that doesn’t use the History API, (like the #more events page for example, where it actually makes sense to use a #hash, or the sample arrangements which load in an overlay via AJAX), so that’s why I created the load_content()
function to take care of all of this.
I should probably also mention that only a small portion of the page (the main content, featured quote in the header at the top, page title, etc.) is loaded with AJAX requests, not the whole page. (I’m using the Kohana HMVC PHP5 Framework and I set my website’s template controller to return a JSON object with all the required parts for every AJAX request, but I’ll save that for another day.) I could have also updated the Twitter updates with each AJAX request, but I decided that keeping the AJAX content as lean as possible was the better way to go. it’s more important that the user has a better experience because content loads faster, than it is for the user to view every single update if I happen to tweet while they’re visiting my site, no?
So that’s partially why I only used the History API on the sub-navigation (and breadcrumb links) and not on the main navigation as well. I could have also used it on the main navigation, but then it meant I’d have to load the sub-navigation into the JSON object as well (since it changes from section to section), but at that point, why not just load the whole page then? I was aiming for quick and efficient after all… So my reasons for not using the History API on the main navigation had nothing to do with the History API itself, but had to do with maintaining the AJAX response at an optimal size.
Benefits and Disadvantages
Since making these changes, (but keep in mind the JavaScript part is just one of the many changes I implemented to make things load quicker and more efficiently), I’ve noticed that the site loads much faster now and the amount of bandwidth my site consumes per visit has shrunk a great deal. I’m not concerned so much about bandwidth in terms of limits, only what in means in terms of speed. Obviously less data to transfer = quicker loads. I think this aspect of the user-experience is much better now, plus, loading content via AJAX also makes viewing the site on a mobile device much more usable. (The mobile version doesn’t load the side content, it’s designed for smaller screens and is much leaner overall – all things that make the pages load faster – and it also uses the History API too.)
The only disadvantage to using the History API wasn’t much of a disadvantage really – I had to add a single line of JavaScript to track page views in Google Analytics for AJAX-loaded content, and I also had to make some minor adjustments to the AddThis code to make sure the correct page is shared. Very trivial things really – both took only seconds to set up.