Turbo charging websites with PJAX

Heads Up!

This article is several years old now, and much has happened since then, so please keep that in mind while reading it.

PJAX?

No that's not a typo! PJAX is a combination of technologies, neatly combining the HTML5 History API (aka pushState) together with AJAX. It offers an almost effortless way to transform the user experience of a website. Reducing load time and making the site feel more like a native app, especially useful on mobile devices.

Why?

Consider a normal page load. A user clicks a link on a page and the browser requests the new page content from the server. When the server responds, the browser removes all the HTML that's needed to display the current page, and replaces it with all the HTML required to display that new page. 

It's highly probable that a large proportion of the HTML is similar/identical to the HTML used on the new page, especially if the site is built using templated layout engines such as razor views or masterpages. Generally, the markup of headers, footers and sidebars remain pretty consistent across pages and the content of resource files such as fonts, CSS & JS change even less.

So why ditch them only to reload them immediately? Reloading adds lag to the browser and can cause a flash as it redraws and reinitialises everything its just binned.

If only there was an easy way to keep the layout and structure that's consistent between pages, and efficiently load just the markup that differs between pages...

PJAX to the rescue

PJAX will intercept a link click and instructs the browser to cancel the full page load. Instead, it retrieves the new content via a single AJAX request. This content (or a subset of it) is then injected into the DOM, and all markup that's consistent between pages is retained.

Finally the URL in the address bar gets updated via pushState, giving the impression that the browser has navigated normally to the page, albeit much quicker, and with less of a flash.

PJAX is also clever enough to test the browser's capabilities, and it will gracefully fall back to normal page loading techniques if it's not supported.

Try it

To demonstrate, I've set up two sites, both running the LocalGov starterkit. One has PJAX enabled, the other is as normal. Go take a look, then return here to find out how to implement it. 

Install

There appears to be a couple of versions of PJAX available online, JQuery-Pjax by defunkt and pjax by MoOx. There are subtle differences between them, but the big one is that one has a dependency on JQuery, which isn't everyone's cup of tea.

I favour the MoOx version for various reasons, but mainly because it has inbuilt support for analytics.

To install this version into your project via NPM, open a command prompt and run:

npm install pjax

Once that's installed, add the script to the head of your templates:

<script src="/node_modules/pjax/src/pjax.js"></script>

Configure

Now we need to instruct PJAX what elements to listen to clicks on, and what elements need their content swapping. In this simple example, I want PJAX to update the page title and swap the contents of the body.

<script>
  new Pjax(
  { 
    elements: "a",
    selectors: ["title", "body"] 
  })
</script>

And that's it! PJAX will begin intercepting links and will swap the content of the body and title elements.

Simples2 1 1 1

Higher Octane

Although the above is the absolute minimum, we can go a little further to squeeze out every last drop of performance.

The server is unaware of PJAX and will still deliver the entire page contents, yet we've told PJAX to only use the title and the body, everything else is thrown away.

PJAX injects a HTTP header and adds a querystring parameter when it issues a request. With those we can differentiate a PJAX request from a standard request at the server side, and reduce the amount of unnecessary markup in the response. A simple way to check would be the following:

public bool IsPjaxRequest
{
  get
  {

    var header = Request.Headers["X-Requested-With"] == "XMLHttpRequest";

    var query = Request.QueryString["pjax"] == "true";

    return header && query;
  }
}

We can then use conditional statements around elements which are unnecessary to PJAX, such as below:

@inherits PjaxDemo.App_Code.PjaxTemplatePage
@using ClientDependency.Core.Mvc
<!doctype html>
<html lang="en">

@{
    Layout = null;

    if (IsPjaxRequest)
    {
        @RenderTitle()

        @RenderBody()
    }
    else
    {
        Html.RequiresCss("~/css/bootstrap.min.css", 1);

        Html.RequiresJs("~/scripts/jquery-1.11.3.min.js", 1);
        Html.RequiresJs("~/scripts/bootstrap.min.js", 2);

        <head>
            <meta charset="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />

            @RenderTitle()

            @Html.RenderCssHere()

            <!--[if lt IE 9]>
                <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
                <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
            <![endif]-->

            <script src="/node_modules/pjax/src/pjax.js"></script>
        </head>
        <body>
            @RenderBody()
            @Html.RenderJsHere()
			<script>
			
			new Pjax( {selectors:  ["title", "#pjaxContainer"]});
			
            </script>
        </body>
    }
}
</html>


@helper RenderTitle()
{
    <title>
        @Umbraco.Field("title")
        @Umbraco.Field("siteName", recursive: true, insertBefore: " - ")
    </title>
}

When the above content is requested via PJAX, we only output the title and body, skipping any of the CSS, JS and contents of the head, navigation and footers etc. 

With some thoughtful structuring of your templates, you can really minimise the amount of data returned. I've tried to keep this example simple, but I'm fond of using MVC's _ViewStart.cshtml to switch the Layout of a view to a PJAX friendly one. 

Reducing the response content makes a massive difference to the speed and goes a long way to making the site feel faster and much more like an app. It also reduces your (or your clients) bandwidth bill!

Advanced Use

I've kept it simple, but PJAX has a vast array of configuration options which are well documented on the github/npm pages. Probably the most important features to note are the events and support for analytics.

Although this version of PJAX should trigger page view tracking in Google Analytics, other tracking scripts may cease to function. However, utilising the events that PJAX fires at various stages of the lifecycle you can easily get them working again. As always it's imperative to fully test your implementation.

Real world use

I've put PJAX into production on a couple of sites now, and it's had a fantastic result. My favourite by far is www.leedsbradfordairport.co.uk it really lends itself well to the design of the site, and totally transforms the feel especially on a mobile device. Ironically, we had to implement a loading bar to provide feedback to users as the pages were loading too quickly! 

At the beginning of the year, I did some investigation into the performance of PJAX on the site. In the three months of data I analysed, I found that using PJAX had reduced the bandwidth by 325GB or around 30%, equating to well over 1TB saved per annum. With some hosting providers, that could be quite a financial saving.

Additionally, there are less HTTP requests per visitor, so the server can support more simultaneous users, meaning less hardware is required.

The most impressive statistic however, is the total amount of time saved just because the pages load faster with PJAX. In three months that totalled a massive 367 hours! That's a lot of time saved for your visitors, and there's oodles of research to indicate that faster loading can increase conversion rate.

Conclusion

So that's PJAX. I hope you've enjoyed reading this, and that I've intrigued you enough for you to go try it for yourself. If so, you can get a copy of the demo site on my github, and feel free to hit me up with any questions or comments.

Merry Christmas!

Tom Pipe

Tom is on Twitter as