This is a guest post by Jonas Sicking, one of the Gecko developers.
As I’m sure you know we’re getting ready to ship Firefox 4. And as you
might know Firefox 4 includes the history API (which includes the pushState() and replaceState() methods) defined in HTML5. This API is also implemented in Safari and Chrome, but Firefox 4 has important differences, which I describe in this post.
A few weeks ago someone discovered a pretty big flaw in the pushState API. The problem is that if you use the state argument to pushState() or replaceState(), and the user later reloads page with such a state, there is no way to get access to said state until after the
load event fires. This because the only way to get access to said state is through the
popstate event which doesn’t fire until after
load has fired.
This means that for pages that use the state argument, the page has to render without knowledge of said state, and only after the page has been fully loaded can the correct state be shown to the user.
Note that the “state” that I’m talking about here is the state argument passed to pushState()/replaceState(). The URL (which arguably is the much more useful argument to pushState()/replaceState()) is always accessible using the normal APIs like document.location and window.location.
To fix this problem we are making two changes in our implementation compared to the current working draft:
- Always expose the current state through a
window.history.stateproperty. This way a page immediately gets access to the current state of the page and doesn’t have to wait until the first
- Don’t always fire a
popstateevent right after the
Instead, only fire it during real session history transitions (i.e., when the user clicks Back or Forward or when history.back()/forward()/go() is called)
The whole purpose of this extra
popstateevent was to give access to the page’s state. However the
window.history.stateproperty makes this redundant. We have found that pages just find this event unexpected and a source of bugs.
The first change should be fully backwards compatible since it’s a purely additive change. It doesn’t affect existing code, which presumably doesn’t use this property.
The second change is the bigger concern. If your code is expecting this event to always fire, then this might result in problems. Another thing that alleviates the risk with this change is that Safari 5 appears to have misunderstood the working draft on this issue, and doesn’t fire this
popstate unless a state is specifically passed to pushState()/replaceState(). So basically Firefox will behave like Safari 5 as long as you don’t use the state argument.
We are also making a third change:
popstateto fire while the page is loading.
The working draft currently has a somewhat surprising limitation in that it forbids any
popstate events from firing before the
load event for a page has fired. If the user clicks on a pushState-backed link while the page is loading (for example due to a slow-loading image), and then presses the Back button, no
popstate event fires. Only after the
load event for the page has fired is the first
popstate allowed to fire. We have removed this limitation and always fire
popstate when the Back or Forward button is pressed or when history.back()/forward()/go() is called.
I have done some testing and so far have not seen any problems due to these changes. Unfortunately, due to discovering these problems so late, these changes won’t appear in Firefox betas until Firefox 4 RC. There are test builds available, which you can test with right away.