HTML5 History in IE10

Building fast and functional sites is a challenge with which most Web developers are familiar. Loading a new page every time the user clicks a link is slow. Fetching all content dynamically effectively disables the back button. Working with hashes is better, but still not ideal. Internet Explorer 10 in the Windows Developer Preview eliminates the compromise by adding support for HTML5 History. The pushState, replaceState, and popstate APIs provide fine-grained control over the behavior of the back button and the URL presented to the user for dynamic content. Together these APIs help you improve the performance of your site without sacrificing usability.

If you’re not already familiar with the HTML5 History APIs, think of pushState as being the dynamic equivalent of navigating to another page. Similarly, replaceState is much like location.replace. The difference is these APIs leave the current page intact when updating the session history by storing states instead of pages. Both pushState and replaceState take three parameters: a data object, a title, and an optional URL.

history.pushState(data, title, url);

history.replaceState(data, title, url);

Note that the title parameter to pushState and replaceState is ignored by most browsers, including IE10. If you want, you can still provide this information since future browsers may expose it as part of their history UI.

Setting Custom URLs

The URL parameter to pushState and replaceState can be used to update the URL of the page without navigating. To illustrate, consider you’ve loaded “http://www.contoso.com/index.html.” Using hash changes, you can only append to the URL:

// Change to "http://www.contoso.com/index.html#about.html"

location.hash = "about.html";

But with pushState and replaceState you can point to a completely different page on your site without actually going there:

// Change to "http://www.contoso.com/about.html"

history.pushState(null, "About", "/about.html");

Make sure your server can handle all dynamic URLs you create so things like favorites still work. You can also add some data to the state object so you don’t have to parse the full URL later to restore the state:

history.pushState("about.html", "About", "/about.html");

The protocol, hostname, and port must match the current URL, but the path, query, and fragment are fair game for customization. This enables associating dynamic states with URLs that are easily backed by the server and work even when script is disabled. Ultimately, this lets you dynamically fetch and display only the data that changes from page to page while keeping the user experience intact.

Restoring Saved States

You should restore state after navigating the history (for example, when the user presses the back button) or reloading the page. Restoring state is accomplished by listening to the popstate event. The popstate event fires when state changes as a result of history navigation. At this point the data object for the destination state can be retrieved via history.state. In cases where the page is reloaded the popstate event will not fire, but history.state can still be accessed at any time during or even after the load. Thus code like the following can restore state at the appropriate times:

function init() {

/* ... */

// Handle page load and reload

loadState();

// Listen for history navigations (e.g. the back button)

window.addEventListener("popstate", loadState, false);

}

function loadState() {

// Grab the data for the current state so we can restore it

var state = history.state;

/* ... */

}

init();

Storing Complex, Dynamic Data

The data object stored in a state can be more than a string. Custom JavaScript objects and even some native types such as ImageData can also be used. The data provided is saved using the structured clone algorithm, which preserves complex relationships such as cycles and multiple references to the same object. This makes saving and restoring even complex objects a breeze as illustrated in this simple demo. In the demo, snapshots of a canvas are captured to create an undo stack using code like the following:

function init() {

/* ... */

// Handle page load and reload

loadState();

// Listen for history navigations (e.g. the back button)

window.addEventListener("popstate", loadState, false);

}

/* ... */

function stopDrawing() {

// Take a snapshot of the current state as an ImageData instance

var state = context.getImageData(0, 0, canvas.width, canvas.height);

history.pushState(state, "");

/* ... */

}

function loadState() {

// Grab the data for the current state so we can restore it

var state = history.state;

/* ... */

if (state) {

// Restore the canvas to our saved ImageData state

context.putImageData(state, 0, 0);

}

}

To change this to keep track of the current state without tracking each change, you can use replaceState instead of pushState.

Size Considerations

HTML5 History makes pushing large amounts of data onto the stack easy if you’re not careful. For example, the undo demo above stores ~0.5MB per state and could easily use more if the canvas was larger. This data will consume memory as long the associated state entry remains in the session history, which can be long after the user leaves your site. The more data you store, the sooner a browser may start clearing your entries out to save space. In addition, some browsers also enforce a hard limit on the amount of data that can be stored with a single call to pushState or replaceState.

Cross-Browser Considerations

As always, use feature detection to handle differences in support across browsers. Since most of HTML5 History involves events and properties, the only new parts that truly require detection are calls to pushState and replaceState:

function stopDrawing() {

var state = context.getImageData(0, 0, canvas.width, canvas.height);

if (history.pushState)

history.pushState(state, "");

/* ... */

}

Such detection will at least keep your script from failing in older browsers. Depending on your scenario, you may want to start with full-page navigations and upgrade to dynamic content when HTML5 History is supported. Alternatively, you can use a history framework or polyfill to keep the back button working, but keep in mind that not everything can be emulated. For example, dynamic control over the path and query components of an URL can only be achieved via pushState and replaceState.

Note that some browsers support an earlier draft of HTML5 History with two notable differences from the current draft:

The popstate event fires even during page load

The history.state property does not exist

In order to support these browsers, you can fall back to reading the state information off the popstate event itself.

Wrap Up

Overall, the HTML5 History APIs provide a great deal of flexibility for building responsive and usable Web sites. With some care taken for legacy browsers, these APIs can be used today to great effect. Start testing them with your site in IE10 on the Windows Developer Preview and send feedback via Connect.