Selective Content Loading

One of the techniques we talk about in Jump Start Responsive Web Design is called Selective Content Loading (SCL). This technique is really useful in situations where you want to load small pieces of data into an already loaded page in a structured way. When would this be useful?

  • When you have serious bandwidth issues between your server and the end user’s device (for example, on a mobile connection that is moving on a poor network with lots of errors and having to deal with cell handoff).
  • When your pages are largely the same structurally from page to page and simply reloading the content saves many requests.
  • If you have chunked your content nicely and want to simply load in the next piece in the sequence (for example, infinite scrolling on a Twitter feed)

Some of these issues can be dealt with by good caching and using local storage and these should definitely be explored as good practices generally. However, even with smart asset caching you still require server round trips to retrieve a document, all the page HTML still has to be sent to the browser and then the browser still has to render the page.

If your page has only added a couple of additional bits of data (for example, a tweet) or is only changing a small amount of content (for example, the details for a product) then SCL may be a good option for you. This doesn’t work in every circumstance and it also causes a number of possible issues with URLs, browser history and what content gets spidered on a “page” by search engines (if this important to you).

Recapping our approach from Jump Start RWD, this is what we’re going to do conceptually:

  • On the first page request we’ll load up all of the page content including the site navigation, content layout, CSS and JS files as normal.
  • After the page has loaded we’ll override all of the links in our page to make them XHRs rather than a standard request for a document.
  • We’ll then process the response (the XHR response will only be the internal page content in JSON rather than the entire page) and overwrite the content that was in the page.
  • We can then use pushState() to modify our browser history (so the URL updates to something shareable and we can go backwards if needs be).

Here’s an example that should illustrate the point simply. The content has been truncated purposefully in order to keep this concise.

We’re going to set up a page that can load content about books without having to reload the entire page, just the pertinent content. We’ll also use pushState() from the History API to ensure that if the user wants to share or come back to the URL they’ll be able to do so.

To make things simple to express, we’re going to use jQuery for the DOM manipulation and a JavaScript templating library called Handlebars.js. If you haven’t checked out JavaScript templating options and what they can do, Handlebars is an excellent choice to get your feet wet.

The core of our solution relies on the fact that URLs can respond differently depending on whether they are an XHR or a normal HTTP request. If the server gets a normal request then the view will deliver the full HTTP response (containing all the document and then the JS, CSS etc). If the server gets an XHR, it will respond with JSON which only contains data about the book requested.

So, as an example, the standard HTTP response for the “Frankenstein” page looks like this:

<!DOCTYPE html>
<html>
<head>
  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
 
var original = null;
var backtostart = true;
 
  <script type="text/javascript">
      ($(document).ready(function() {
          var source = $("#story-template").html();
          var template = Handlebars.compile(source);
 
          var story_link_handler = (function(evt) {
              evt.preventDefault();
              $.get(this.href, function(data) {
                  $("#contentarea").html("");
                  $("#contentarea").html(template(data));
                  history.pushState({url:data.location}, data.title, data.location);
              }, "json");
          });
 
          $("ul#storylist li a").bind("click", story_link_handler);
 
          $(window).bind("popstate", function(evt) {
              if (event.state) {
                  url = event.state.url;
                  $.get(url, function(data) {
                      $("#contentarea").html("");
                      $("#contentarea").html(template(data));
                  }, "json");
               backtostart = false;
              } else {
               if (! backtostart) {
                  backtostart = true;
                      $("#contentarea").html("");
                      $("#contentarea").html(original);
               } else {
                 original = $("#contentarea").html();
                    backtostart = false;
               }
            }
          });
 
      }));
  </script>
</head>
<body>
  <ul id="storylist">
      <li><a href="mobydick">Moby Dick</a></li>
      <li><a href="gulliverstravels">Gulliver's Travels</a></li>
      <li><a href="frankenstein">Frankenstein</a></li>
  </ul>
  <div id="contentarea">
      <article id="story">
          <h1>Frankenstein</h1>
              <h2>Mary Shelley</h2>
              <p>Doctor creates artificial life</p>
          </article>
      </div>
<script type="text/javascript" src="handlebars.js"></script>
      <script id="story-template" type="text/x-handlebars-template">
      <article>
          <h1>{{title}}</h1>
          <h2>{{author}}</h2>
          <p>{{synopsis}}</p>
      </article>
      </script>
  </body>
</html>

NB you can download code used in this article in a zip file linked at the end of this article

However, the equivalent JSON response for an XHR will look like this instead:

{
  "location": "/frankenstein",
  "title": "Frankenstein",
  "author": "Mary Shelley",
  "synopsis": "Mad doctor creates artificial life"
}

All the code required to make the selective loading work is requested and loaded in the first request. After that, we only get the data and then load it into the template. Let’s take a look at how the code works.

  <script id="story-template" type="text/x-handlebars-template">
      <article>
          <h1>{{title}}</h1>
          <h2>{{author}}</h2>
          <p>{{synopsis}}</p>
      </article>
  </script>

NB you can download code used in this article in a zip file linked at the end of this article

Handlebars uses a script element to create a template for what an article looks like (this content won’t be rendered by the browser as it won’t take an action on its type). Variable locations are defined using {{variable}} syntax. You can do a lot more with Handlebars (conditionals, loops, block execution etc) that we aren’t using in this instance though. Note the ID for the script, we need this so we can pass it into the Handlebars template compiler.

In our document ready function, we grab the HTML from the template script tag we defined above and then we compile it into a template object we can use with new data later.

  var source = $("#story-template").html();
  var template = Handlebars.compile(source);

Next, we define a function we can use for our link onclick event handler. Here we’re simply stopping the actual request to the file by preventing its default behaviour. From there we make a jQuery XHR that returns JSON to the URL that was defined in the link’s HREF attribute.

  var story_link_handler = (function(evt) {
      evt.preventDefault();
      $.get(this.href, function(data) {
          $("#contentarea").html("");
          $("#contentarea").html(template(data));
          history.pushState({url:data.location}, data.title, data.location);
      }, "json");
  });

When the response comes back, we simply overwrite the div content area that holds all our book data.

$("#contentarea").html(template(data));

We also use pushState() to push the URL we just requested onto the browser history so we can go backwards using the back button.

history.pushState({url:data.location}, data.title, data.location);

That’s not quite the whole picture with pushState(), though, in order for it to “just work” for the user. We next create a popstate event handler on the window so that when the user hits the back button we can update the content with the appropriate data for the book. In this case we’re going and getting the data again using an XHR. With pushstate, it’s possible to store data in a state object. In our case the amount of data is small and it’s bad practice to load up the user’s browser with additional data (especially on mobile) so only do it if you can guarantee it’s a tiny amount.

  $(window).bind("popstate", function(evt) {
      if (event.state) {
          url = event.state.url;
          $.get(url, function(data) {
              $("#contentarea").html("");
              $("#contentarea").html(template(data));
          }, "json");
      }
  });

One of the things we need to consider with this technique is what happens when the browser gets back to the start of the list. That is, you’ve popped all of your XHRs off the stack and you’re back to where you started.

To remedy this, we use a flag to determine if we’re back to the start or not and we save the content that was in #contentarea so we can replace it. You can use other techniques such as simply hiding the original content area or storing the original document’s JSON.

We then update the popstate event to check if there’s no event.state. If so, we revert to our original form.

$(window).bind("popstate", function(evt) {
              if (event.state) {
                  url = event.state.url;
                  $.get(url, function(data) {
                      $("#contentarea").html("");
                      $("#contentarea").html(template(data));
                  }, "json");
               backtostart = false;
              } else {
               if (! backtostart) {
                  // revert the content to the original
                  backtostart = true;
                      $("#contentarea").html("");
                      $("#contentarea").html(original);
               } else {
                 // store original content to retrieve later
                 original = $("#contentarea").html();
                    backtostart = false;
               }
            }
          });

Finally, we add our click event handler to all the relevant links. In our instance, we’re just using the links in the list, but in practice you could do this to a whole range of links based on class or HREF attributes.

$("ul#storylist li a").bind("click", story_link_handler);

The rest of the page is the page structure and the actual content that was requested – in this case the /frankenstein URL.

As can be seen, this approach gives us a nice, responsive setup. The initial page request is a little heavier (in this case about 1Kb) but provides all of the scaffolding needed to layout the page and provide the interactions. Subsequent requests get the benefit of only having to return very small snippets of data, which are then loaded into the template.

The URL is updated using pushState() which means the user can still share the URL using intents or copy and paste and that URL will work properly for whomever it is shared with. You also get the benefit that each document still exists properly – this means search engines can still correctly index your site, if that’s needed.

One of the things we need to be careful of with this technique is that if we have content that exists in many different templates, the benefits from only loading the snippets of data via XHR will be destroyed by having to load all of the different content templates into the first page request and masking it from the user until it’s used. Don’t forget, all of the HTML still has to be loaded, regardless of whether it’s used or not.

Where this technique works particularly well is in an “infinite scroll” scenario such as a stream of content or very long article. The content type doesn’t change almost at all (or only within a very defined way)—this technique does for “page content” as a whole what the lazy loading technique does for images within a page. If you’ve taken the time to chunk your content, this can be especially effective as it means you can avoid the user hitting “goto page 2” even though search engines will follow happily.

Download code files used in this article

Delve further into the world of responsive web design in Andrew’s new book with Craig Sharkie: Jump Start Responsive Web Design.

Win an Annual Membership to Learnable,

SitePoint's Learning Platform

  • http://learnwebtutorials.com Blissful Writer

    The link to handlebars.js should be http://handlebarsjs.com

    • http://www.onsman.com Ricky Onsman

      Thanks, BW, that’s fixed (my mistake, not the author’s).

  • http://www.braillemedia.co.uk John Sexton

    Great article, although I would like to add that if the tab focus is moved when a link is activated to move the tab focus to the top of the new content, it makes it much more usable for screen reader users.

    Please see full code below:

    var original = null;
    var backtostart = true;

    ($(document).ready(function() {
    var source = $(“#story-template”).html();
    var template = Handlebars.compile(source);

    var story_link_handler = (function(evt) {
    evt.preventDefault();
    $.get(this.href, function(data) {
    $(“#contentarea”).html(“”);
    $(“#contentarea”).html(template(data));
    $(‘#start’).focus();
    history.pushState({url:data.location}, data.title, data.location);
    }, “json”);
    });

    $(“ul#storylist li a”).bind(“click”, story_link_handler);

    $(window).bind(“popstate”, function(evt) {
    if (event.state) {
    url = event.state.url;
    $.get(url, function(data) {
    $(“#contentarea”).html(“”);
    $(“#contentarea”).html(template(data));
    backtostart = false;
    }, “json”);
    } else {
    // no state so we use the original
    if (! backtostart) {
    backtostart = true;
    $(“#contentarea”).html(“”);
    $(“#contentarea”).html(original);
    } else {
    original = $(“#contentarea”).html();
    backtostart = false;
    }
    }
    });
    }));

    Moby Dick
    Gulliver’s Travels
    Frankenstein

    Frankenstein
    Mary Shelley
    Doctor creates artificial life

    {{title}}
    {{author}}
    {{synopsis}}

  • Tony

    Just wondering what the SEO implications are for this approach…

    • Joe

      There would be no SEO implications as we are updating the history (an essentially moving to a ‘new’ page), therefore, a web crawler such as Google’s, should still be able to index your website. I can attest to this as I have a similar setup on my company’s website that I have setup.

  • http://www.brothercake.com/ James Edwards

    How does this square with accessibility — adding content to the page via JS that doesn’t really need to be dynamic — isn’t that just creating an accessibility barrier where they needn’t be one?

    btw. I don’t agree with moving the focus to generated content, as has been suggested. You certainly should ensure that generated content is inserted in a logical place, so that it comes next in the Tab order, but you shouldn’t presume to actually move the focus there yourself.

    • http://www.braillemedia.co.uk John Sexton

      Hi James,

      I’m interested to learn your reason behind not using moving of user focus when used in the context of activating a link. I understand the idea of not taking away user control as it is far better to enpower the user than to assume user requirements.

      As far as I can see it in this case, the js click event does no more than a named page anchor link would do for static content. I would suggest that this specific example would work better if backed up by a server-side script for agents without js support enabled, which would also help for seo.

      I would also say the addition of a back named page anchor link at the end of the content may benefit users to help orientate their focus back to the initial links. Much the same way as old static q/a pages would use named links at the top of the page to jump to the content and at the end of the content a back to top link allows the user to navigate back to the list of links.

      I think there may be a few cases where this kind of dynamic content could improve user load times, but it wouldn’t be sencible to base an entire site on this idea. Things like text streams, q/a data may be some good uses but having a server-side alternative would mean that users without js will not lose out.

      The server-side solution could use the same json data source but being server-side would require a full page load.

      Being a screen reader user myself, one issue with js dynamic content which is activated via a click link event, is if it is not made clear within the link text that new content will follow after the link, it can seem that the link has had no effect. Therefore, by moving the focus, it gives immediate feedback much like a skip to link.

      Best, John