7

I have a page that the user can modify. All modifications are performed using JQuery, and also sent to the server, so that a full reload will produce the modified page too.

This works fine in Firefox 11 / Chrome on Windows: even if the user navigates somewhere else and then uses the "Back" button, they get the page with their latest edits.

However, if I now embed Google Maps onto the page, the Back button stops working: it takes the user to the page how it was before all their edits. This page doesn’t even exist anymore except in browser’s cache, and yet it gets displayed.

I’ve put together a simple testcase here that shows this behaviour.

What gives? How can I fix this? The perfect solution would just allow the browser to go Back without reloading the page, like it would do normally.

P.S. Apparently the "working" example doesn’t actually work in Chrome on OSX either. How can I work around the browser’s insistence on going back to a stale version of the page?

Bug reports describing this behaviour: Firefox


Bounty: Firefox and Chrome on Windows exhibit both behaviours (going back to the modified DOM in one case, but unmodified in another). Is there a spec describing what the browser should do? Are there bugs filed to change this one way or another? Does this issue have a common name that I can google?

I’m considering a solution whereby I update a hidden element via JavaScript, and then check if the update is still there. If so, the "Back" button restored up-to-date DOM, and nothing else needs to be done. If not, the browser restored outdated DOM, and I can just force a page reload, as unpleasant as that is. Any comments on this approach are also welcome.

Note: the real website has more editable controls than that, and one of them is a freeform text area. I would like the proposed solutions to work even if the user has just added several paragraphs of text. That kind of thing can’t be appended to the URL after the #, for example.

Roman Starkov
  • 59,298
  • 38
  • 251
  • 324
  • have you considered loading the initial state of the forms also via ajax? – kritzikratzi Mar 24 '12 at 20:34
  • @kritzikratzi There are two ways to do that. One: embed the necessary data into the page (which I think has the same problem as the current approach), or two: request it once the page has loaded (which makes the page takes at least twice as slow). I’m not happy with either. – Roman Starkov Mar 25 '12 at 09:55
  • i see. have you tried to add the unload listeners? i've never used it, but apparently simply registering the listener disables the cache in many modern browser (see here for instance: http://www.webkit.org/blog/516/webkit-page-cache-ii-the-unload-event/ ) – kritzikratzi Mar 26 '12 at 01:30

7 Answers7

5

Embedding Google Maps into the page disables the bfcache (I'll use the Mozilla term for lack of a standard one) because the maps page loaded in an <iframe> uses an unload listener.

The possible reasons for a page not being cached for fast back navigation in Firefox are listed on MDN: Using Firefox 1.5 caching. Your problem is listed as "the top-level page contains frames that are not cacheable", which is confusing, I'll try to clarify it later. (Other browsers probably use similar heuristics, since these rules were developed to avoid breaking existing content - see also this answer, it has some links.)

The correct way to fix this would be to befriend someone at Google and then nag them until they remove the onunload listener at least from maps' embedded pages.

In general you shouldn't ever rely on bfcache working or not working for a particular page. It's just an optimization for the common case. Since it's an optimization, it could be disabled, for example when the system is low on memory. It also won't work if the user restarts the browser before going back, or closes the tab and picks 'undo close tab', as you noted in the bug.

You should either restore the page's state from JS or mark the page as not cacheable (using an HTTP header). The former results in a better user experience, of course. @Adam Gent's suggestion looks correct, I'll have to check what Firefox problem he refers to.


The reason bfcache works this way is:

  • If the browser runs an onunload handler, then restores the page via bfcache, the page could be broken, since scripts often remove the event listeners in the onunload handler (to "clean up", which is not really necessary except in old IE versions)
  • if the browsers stopped running onunload handlers in a page on the basis that the user might return to the page and they'd like to cache it, the authors would complain.
  • if a page in an iframe can't be cached, restoring a cached outer page and reloading the inner page would break them sometimes (e.g. if both pages are same-domain, the outer page can hold references to objects in the frame, which won't be valid after the inner frame is reloaded). So if an iframe is not cached, neither is the parent page.

The reason the page is still loaded from the (disk) cache when you hit "back" is that presumably you specified that the content you sent to the browser can be cached. The browser has no way to know that you update the page on the server in parallel with making the DOM changes to it.


[edit]I'll elaborate on the "mark the page as not cacheable" idea above. To make web browser cache work for, and not against you, it's important to remember that HTTP is a protocol for retrieving resources. For example, the HTML page identified by the URL http://bbb.akshell.com/broken is a resource. When you serve a resource over HTTP you specify how long the browser's copy of the resource will be valid (i.e. matching the canonical version of the resource on the server).

When, as in your testcase, the resource is an HTML page with the item selected by the user marked up in a special way, the resource may change at any time (every time the user changes the selection). This means that the honest HTTP response when serving this resource would be "don't cache, may change at any time". The browser would then reload the page from the server every time it needs to load the page -- correct behavior, but at the cost of slowness for the user.

An alternative approach, appropriate for things like switching between multiple in-page tabs would be to associate each selection with its own URL. A page with two tabs would correspond to two resources (and two URLs) -- one for each tab. Both resources could be (HTTP-)cached by the browser. Changing the URL and the page's content could be implemented without the roundtrip to the server via pushState.

Another approach, which seems more applicable to your case, in which you save the user's input on the server: separate the app UI and the user's data into different resources (i.e. a static (HTTP-)cacheable HTML page with JS, loading the user data from a separate non-cacheable URL). Request the user data and update the UI on load.[/edit]

Nimantha
  • 6,405
  • 6
  • 28
  • 69
Nickolay
  • 31,095
  • 13
  • 107
  • 185
  • Right, so if the page is restored from the bfcache then the DOM changes are preserved, but if it's restored from the disk cache, the DOM changes are lost. Correct? So to fix the navigation, I'd have to either force the bfcache off, or detect whether it got used, and if not, force a page reload / re-apply changes via JavaScript? – Roman Starkov May 06 '12 at 17:45
  • "It's just an optimization for the common case." – my website has to explicitly _support_ this optimization, not ideal... Shame that DOM state caching is not properly specced out and not really very much under my control; browsers are free to restore or dump it. – Roman Starkov May 06 '12 at 17:49
  • 1- correct, 2- not exactly (see updated answer): forcing the bfcache off wouldn't help; 3- rather, the web site has to avoid doing things that are *incompatible* with caching. You probably already saw https://bugzilla.mozilla.org/show_bug.cgi?id=738599#c3, but I'm linking to it for the sake of other readers. 4- if browsers *had* to store the runtime state of pages, everyone would complain about the amount of memory they used. – Nickolay May 06 '12 at 23:36
  • Hmm... but with the headers I'm sending, the browser does not seem to read the page from the cache; it actually re-requests it. In other words, I did _not_, I think, promise the browser that the page won't change. – Roman Starkov May 07 '12 at 00:24
  • I'm confused. Are you talking about http://bbb.akshell.com/broken ? I don't see any cache-related headers in HTTP response for that URL, meaning that the browser uses heuristics when deciding whether to re-load from the server. (I'm not actually an HTTP expert, so I'd have to read through its RFC again in order to be of more help.) – Nickolay May 07 '12 at 00:31
  • Yes, I am. According to HttpFox, the browser does not use the cache if I just press Enter in the URL bar; it actually re-fetches it. But anyway; perhaps those heuristics happen to be slightly different to the bfcache heuristics. I'll try marking the page as no-cache explicitly as a quick fix, and consider more advanced approaches later. – Roman Starkov May 07 '12 at 00:54
  • "slightly different to the bfcache heuristics" -- right, except you meant "heuristics used when navigating back to the page", not "bfcache". We've switched to talking about the HTTP-level cache here, which is entirely unrelated to the bfcache. – Nickolay May 07 '12 at 02:30
1

This trick will clear bfcache for FireFox:

window.onunload = function(){}
Nimantha
  • 6,405
  • 6
  • 28
  • 69
Dan
  • 55,715
  • 40
  • 116
  • 154
1

If you don't care about old browsers you could use a combination of local storage and pushstate.

Using @lucian.pantelimon 's solution + pushstate + localstorage should get you what you want.

<script>
window.addEventListener("popstate", function(e) {
    alert("hello"); // do my restoration from localstorage.
});
</script>

@TimWi is right that if the browser doesn't fire an event your kind of "up shit creek".

One thing you might want to investigate is a truly gross hack of a frameset (ala reddit/google images/linkedin article style).

EDIT: Looks like the pushstate events don't get fired off when the back button is pressed on a remote site back to your site.

EDIT 2: It looks like my firefox v 10.0 has problems with pushstate. It works fine in chrome.

Adam Gent
  • 47,843
  • 23
  • 153
  • 203
  • So is this behaviour considered a bug by any browser’s developers? – Roman Starkov Mar 29 '12 at 15:31
  • I'm not sure but I was positive the pushstate events would fire: "Suppose now that the user now navigates to http://google.com, then clicks back. At this point, the URL bar will display http://mozilla.org/bar.html, and the page will get a popstate event whose state object contains a copy of stateObj. The page itself will look like foo.html, although the page might modify its contents during the popstate event." -- https://developer.mozilla.org/en/DOM/Manipulating_the_browser_history – Adam Gent Mar 29 '12 at 15:36
0

Your 'working' page doesn't work for me either--at least not the way you want it to.

It looks like you update the page's DOM via jQuery, then send an update back to the server, but never force the browser to grab the latest from the server. So the back button behavior is entirely dependent on the browser deciding to either cache the last server request, or the last state the DOM was left in.

You could try updating the URI with a query string when you do the DOM update via jQuery. That may force the browser to grab the latest upon return.

DA.
  • 39,848
  • 49
  • 150
  • 213
  • The desired behaviour, and the way the "working" page works for me, is that the user’s selection is reflected immediately (no page reload), and stays there on page reloads, and upon navigating "Back". You’re saying the "working" doesn’t do this in your browser? Which one is it? – Roman Starkov Mar 20 '12 at 01:04
  • Yes, that's what I'm saying. When clicking BACK from google, the previous page is in its original state--not the state after the DOM update. Chrome/OSX – DA. Mar 20 '12 at 01:21
  • Thanks, updated the question to reflect your findings. Yes, I could just scrap ajax completely and do a normal page reload when the user changes the setting, but that defeats the purpose. – Roman Starkov Mar 20 '12 at 01:34
  • well, look into my suggestion of updating the URL when you do the AJAX update. Can't say that will work for sure, though. – DA. Mar 20 '12 at 02:52
  • I’ve looked into it. If I just change the URL, the browser will reload the page. If I use the HTML5 history API, I will end up creating a new entry in the history, which is wrong. – Roman Starkov Mar 20 '12 at 10:16
  • How are you changing the URL? I was thinking along the lines of using location.hash, though this might be a better option: https://github.com/balupton/History.js As for adding a new entry to the history, I think you WANT that, to force the browser to see the page as a separate (and new) state. – DA. Mar 20 '12 at 16:43
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/9101/discussion-between-romkyns-and-da) – Roman Starkov Mar 20 '12 at 17:55
0

Assuming you don't trust the browser, depending on the difficulty on the changes, you could use link name for indicating the page state.

For example, your "Select me!" link #3 would lead to http://bbb.akshell.com/broken#link3. That won't make the page reload. So even if the page is loaded from the scratch after "Back" is pressed, you still have the #link3 in url and you can use it to get the page into the state you want (in this case, fire "Select me!" #3).

You could also generate some more complicated urls to make up for more complicated changes. I believe mechanics like that of some sort are used to navigate onto the specific section of flash website when everything is handled by the same flash movie.

P.S. That is the "I don't trust the browser" solution. If you do trust - a solution needed that works in every one of them, I would be interested to see it as well.

Ranty
  • 3,333
  • 3
  • 22
  • 24
  • The real website has about 15 editable elements, some of which include text. This small example happens to be amenable to that approach, but what if the user has just added three paragraphs of text? – Roman Starkov Mar 22 '12 at 20:16
  • I think two major points here to look into are: a) edit the page layout with DOM changes, not something like `innerHtml=...` and 2) try adding link name to url on every change. I'm not sure if it will work and I'm sorry that I don't have spare time right now to try it out so I thought I'll just shoot the idea – Ranty Mar 26 '12 at 20:15
0

You could try storing as much information as possible in the cookies and "rebuild" the page when needed.

Not quite a clean approach as the cookies will be sent every time you access the page (limiting the amount of data stored in the cookies is a must-do here I think), but it should be cross-browser compatible.

Another way would be to store the data in window.name. For pros, cons and a couple alternatives for this approach look at Using window.name as a local data cache in web browsers.

EDIT: It might be useful to store the states of the application and associate an id with each one, then add something like #stateID<state id here> to the address. When the user clicks the back button, you can know which state to load.

I.E.:

Say your state contains the selectMeItemSelected and mapInfo{latitude, longitude, zoomLevel} information.

When the user first enter the site you save stateID #1 in the cookies (or somewhere else, but I'll be using cookies in this example) as:

 state[1].selectMeItemSelected = 1;
 state[1].mapInfo.latitude = 45;
 state[1].mapInfo.longitude = 45;
 state[1].mapInfo.zoomLevel = 2;

After this you add #stateID1 to the website address.

When the user makes an action that activates your AJAX code, you store another state in the cookies, change the URL to #stateID2 then resume running your AJAX code.

When you detect that the user has clicked the back (or forward) button, just load the page using the info stored in the state.

If you generate the stateIDs sequentially, you should also clear the states that follow the current state before adding the new state. This way you get the behavior of the back and forward buttons in the browser: if you click back from stateX, then click another link (can be AJAX), you won't have an active forward button active to click to go "back" to stateX and you can consider clearing that state.

This approach does not work with bookmarks, unless you store the states on the server and pass the state information each time or implement some kind of permalink.

Nimantha
  • 6,405
  • 6
  • 28
  • 69
lucian.pantelimon
  • 3,673
  • 4
  • 29
  • 46
0

I don’t know whether all browsers will run the domready event (or indeed any event) when you press the Back button and it takes you to a cached page. It is clear that if a browser doesn’t run any events then, there is nothing you can do.

However, my suspicion is that they will run the domready event. If they didn’t, jQuery event hooks that are added via $(function() { ... }) wouldn’t be set, and if that were the case, I probably would have encountered the bug much more (because it would be a lot more visible to me as a user).

Therefore, my suggestion is to handle the windowunload† event; within that event, hook some code to the domready event that checks whether the page is in the modified state or the original, unmodified state; and if it is in the unmodified state, force a reload of the page. (You could, in theory, do this reload in an ajaxy fashion so that it doesn’t affect the browser history.)

You therefore need to mark the page as modified or unmodified. For example, you could add an attribute to the body element every time the page is modified. In jQuery I would recommend to use data:

$(document.body).data('modified', true);

Of course the downside is that you need to do this in every ajaxy piece of code that modifies the page. If you have a centralised function that all your ajaxy DOM modifying goes through, it’s quite a bit easier.

Here’s my attempt for the windowunload† handler:

$(window).unload†(function() {
    $(function() {
        if (!$(document.body).data('modified'))
            forceReload();
    });
});

Of course you’ll have to write forceReload() yourself. It could be a simple location.reload‡() or an ajaxy call.

† I don’t remember the exact name of the event or the jQuery function to hook to it.

‡ Likewise, check whether it was called reload or refresh or something else.

Timwi
  • 65,159
  • 33
  • 165
  • 230
  • There's no point to adding a load-time listener during the unload phase: the act of adding the unload listener will ensure the page is not saved in bfcache, so when the user goes back the page will be reloaded from the disk cache, and no JS/DOM state (not even the domready handler you added) will be restored. – Nickolay May 05 '12 at 23:27