Guillermo Esteves

Web design & stuff.

Better infinite scrolling with the HTML5 History API

Now that Piictu finally launched and is out of beta, I want to write a bit about one of my favorite things I worked on as the front-end web developer there, which is our implementation of an infinite scrolling page improved by the use of the HTML5 History API, the problem it tried to solve, and the solution we arrived at.

What’s Piictu?

A bit of background first. In case you haven’t tried it (and you totally should,) Piictu is an iPhone social photo app that revolves around the concept of “photo streams”, or threads of photos by different users on the same subject. For example, you can take a photo of a sandwich, start a stream titled “eating a sandwich”, and watch as your friends and followers reply with their own photos of their own sandwiches, or whatever they’re having for lunch. Check it out, there are some incredibly creative games and memes going on over there. It’s a lot of fun.

Since these streams could conceivably have hundreds of photos, and we wanted an uninterrupted photo-viewing experience, we immediately decided to implement each photo stream as an infinitely-scrolling page, instead of using regular pagination. However, this concept of streams of thematically-related photos defined one of the main requirements for the design: we never wanted to take a photo out of its context, which meant that when people shared them, we couldn’t have traditional permalinks with just the one photo. The challenge was to figure out the best way to allow a user to share any photo without taking it out of the context of its stream.

The problem with infinite scroll

I’m not a big fan of many sites’ implementations of infinite/endless scroll, and given a choice, I turn it off. Most times it just drives me nuts. For example, in most sites that use it, if my Internet connection goes out or there’s a server error or my browser crashes, I’m forced to start back at the top, which I find infuriating if I’m really deep down the page. Another problem is that I usually can’t bookmark my position, so if I leave and come back later, I’ll have to start over. So, in addition to the photo-sharing-on-an-infinite-page problem, I also wanted to tackle these issues, for a better user experience.

The old Ajax way

My first idea when tackling this problem was a traditional solution using Ajax and fragment identifiers, so we could start the stream of photos at an arbitrary point defined by storing the ID of the desired photo in a URL hash (e.g. /stream/123/#/photo/456.) Since anything after the hash (#) character, or fragment identifier, in a URL isn’t sent to the server, this would require passing the photo ID to the server using Ajax, and loading the correct photos in the sequence with JavaScript. To make sharing easier, I wanted the hash fragment to be updated with the ID of the photo currently in the viewport as the user scrolls up and down, so they could share it by simply copying and pasting the URL.

However, I had a few issues with this approach. The first obvious one was that it doesn’t degrade gracefully. If the visitor doesn’t have JavaScript enabled or an error prevents the JavaScript from loading, then the user will get a nice empty page – probably not the best experience. It also prevents the page from being crawled, not just by Google, but also by Facebook. When sharing or liking a page, Facebook determines what title, description, and thumbnail to display in the News Feed by crawling the page and looking for Open Graph tags, falling back to things like the <title> tag, description meta tags, and other images on the page. On a traditional permalink page like Instagram’s, it’s easy, just set the Open Graph tags with the metadata of the one photo in the page. But on a page with a multitude of Ajax-loaded photos, without the server knowing which photo is being requested (remember, the server never gets the hash fragment,) how do you set these tags? If Facebook can’t see that information for the photo being shared, it wouldn’t know what to display in the News Feed, undermining what we set out to accomplish in the first place, which was to make it easier for users to share the photos. As an up-and-coming startup looking to get traffic and exposure, this was a real deal-breaker, so I quickly scrapped this solution.

A better solution using the HTML5 History API

Instead, I decided to use the HTML5 History API. Instead of getting the ID of the photo currently in the viewport and using it to change the fragment identifier, I update the URL in the address bar by calling the replaceState() method. The basic idea is this:

  1. Wait for the scroll event to fire. (Note that since the scroll event can fire a lot, for performance reasons it’s best to run any code attached to this event after a small delay, using a setInterval, as per John Resig’s recommendation.)
  2. When the page has scrolled, get the ID of the top-most photo in the viewport. For this I used the Viewport Selectors jQuery plugin, which adds a handy :in-viewport selector. I also embedded the ID of each photo as a data-photo-id attribute in their markup, to make it easy to get with JavaScript.
  3. If the browser supports the History API, use replaceState() to add the photo ID to the base URL of the stream page, or remove it, if it’s the first photo in the stream (i.e. if we scroll back to the top.) The reason I chose to use replaceState() (which updates the current browser history entry) instead of pushState() (which adds a new history entry) was because I didn’t want to have to click “back” a bunch of times and go back through every photo just to get to the previous page.

An abridged version of the JavaScript code used in Piictu looks somewhat like this (I removed some functionality that wasn’t really necessary for the History API explanation from the code, such as the actual infinite scroll implementation. I hope it’s clear enough):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$(window).scroll(function(){
  did_scroll = true;
});

// Every 250ms, check if the page scrolled
setInterval(function(){
  if (did_scroll) {
      updatePhotoPath();
      loadMorePhotos(); // Infinite scroll, etc.
      did_scroll = false;
  }
}, 250);

// Update the URL
function updatePhotoPath() {
  var new_url;
  var in_viewport = $('div.piic:in-viewport').first();
  if (history.replaceState) {
      if (in_viewport.hasClass('original')) {
          new_url = base_url; // The original URL of the stream page
      } else {
          new_url = base_url + "/photo/" + in_viewport.data('photoId');
      }
  history.replaceState('', '', new_url);
  }
}

You can see this in action by going to any stream on Piictu, such as this Hipstamatic stream I started a few months ago. As you scroll up and down, you’ll notice that the ID of the photo in the viewport is appended to the URL of the stream page, and when you return to the top, it’s restored to the original URL (the base_url variable in the source code, which is also saved in a data-* attribute in the markup for easy retrieval.)

Screenshot of a Piictu stream page

So what happens on the server when we request a stream? If we request a plain stream URL, such as /streams/123, the server returns the first few photos normally, starting with the first photo in the stream. If we request a URL that contains a photo ID, like /streams/123/photo/345, the server again returns a few photos, but this time starting at the photo with the specified ID, with an option to load the photos above it, or scroll down to load more photos below. No need to use JavaScript to figure out which photos to show, it’s all returned directly from the server. Also, the metadata of the photo being requested is also returned as Open Graph tags in the <head> of the page, so when you post /streams/123/photo/345 on Facebook or Google+, they’ll show the correct thumbnail and caption for that photo. It solves our goal for the photos, which was to help users to easily share them: regardless of whether they use the sharing buttons next to each photo, or simply grab the URL from the address bar and paste it in an instant message or their favorite social network, it’ll just work.

It also alleviates some of my pet peeves with infinite scrolling. Since the URL updates automatically as you move up and down the page, you can easily bookmark your position, which is particularly handy on very long streams; and if for whatever reason you’re forced to reload the page or your browser crashes, you’ll start where you left off, avoiding the frustration of having to start over (assuming your browser reopens your tabs after a crash.)

Finally, it degrades somewhat gracefully, as it’ll show the appropriate photos even if JavaScript is disabled, since JavaScript isn’t necessary to figure out which photos to load. (I say “somewhat” because it doesn’t yet offer regular pagination as a fallback, but it’s on the to-do list.)

What about Internet Explorer?

As always, the biggest issue with using any modern technology is Internet Explorer, since in this case it doesn’t support the History API in versions 9 and below. I briefly worked on a workaround for IE, using the ol’ hash fragments as a fallback. In the end we simply decided not to support IE, mainly because between January and May, Internet Explorer accounted for only 2.42% of the visits to our signup and teaser page, so the added effort and maintenance it would require seemed counterproductive. In addition, our implementation degrade gracefully in IE. The URL may not change as the user scrolls, but everything else works properly and sharing photos is still possible, using the Twitter and Facebook buttons. In other words, it simply behaves as a traditional implementation of infinite scroll. Finally, it’s a temporary situation, as Internet Explorer 10 will support the History API, and it shouldn’t require any further work. I tested it in the Windows 8 Developer Preview, which includes a preview version of Internet Explorer 10, and it worked perfectly.

Conclusion

I really believe that using the HTML5 History API to augment infinite scrolling offers a superior user experience by alleviating some of the annoyances caused by traditional approaches, such as the lack of bookmarking and sharing. I expect this technique will be used more once Internet Explorer supports the History API, but if you’re willing to live without IE support for a bit (or use one of the many polyfills available,) it’s definitely worth giving it a try now.

Let me know what you think about this; I look forward to your comments and questions, even though I still haven’t gotten around to adding comments to this blog. In the meantime, feel free to tweet @gesteves or send me an email.