JavaScript
Article
By Andrew Fisher

Selective Content Loading

By Andrew Fisher

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).

--ADVERTISEMENT--

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.

Recommended
Sponsors
The most important and interesting stories in tech. Straight to your inbox, daily. Get Versioning.
Login or Create Account to Comment
Login Create Account