Modern browsers use the back/forward cache (“bfcache”) to accelerate simple ⇦ and ⇨ navigation. Confusingly, the bfcache is different from both the classic HTTP cache and the new Cache API, and its impact on 3rd-party JS isn’t widely measured.
The bfcache is dynamically managed and cleaned up by the browser: it doesn’t have user-tweakable settings and can’t be directly turned off. There are also lots of different reasons that the same page might be bfcache-able on one visit but not on the next.
This all gives the bfcache a black-box vibe. But the bfcache usually leads only to Good Things™ so people don’t worry about it, aside from some devs trying to make sites as bfcache-able as possible.
Interestingly, Munchkin’s backward compatibility (pageview and link tracking still work with old browsers) conflicts with the bfcache design. This wasn’t deliberate, of course, but the product of different goals.
In this recording, check out when visitWebPage and clickLink events stop being sent to Marketo:
The initial visitWebPage fires, as do clickLink events for the top 2 internal links. When I click the 3rd off-page link (“This is a link to another page”) there’s a clickLink for that, too. Then I use the back button on my mouse (you can’t see that!) to return to the initial page. Upon returning, no events are fired, though I’m interacting with the same links.
Munchkin unhooks click events out of an abundance of caution
See, when Munchkin detects the page is about to unload, it unhooks itself (i.e. removes event listeners) from all interesting <a> elements and from the main document to avoid potential memory leaks. Using the beforeunload or unload event to free memory wasn’t controversial in the past. While the majority of browsers — even in those old days — didn’t have problems collecting memory, a few did, and there were no downsides.
Then came the bfcache. The bfcache changes what it means to “unload” a document. Now, unload can mean either:
- the document is going away forever and its current state (DOM/JS/memory) is ready to be garbage collected (the old meaning); or
- the document will be restored from a snapshot of its current state and resume running if a back/forward action — on-screen button, mouse button, or touch gesture — happens within a short time window (after that window, the document is removed from the bfcache and behaves as in (1))
It’s case (2) you’re seeing above. Munchkin, worried about memory leaks, removes all DOM listeners before the page unloads. As the page is bfcache-able, the browser caches its final state — that is, without the listeners. A back action is used quickly, so the bfcache’d state is restored rather than loading the page over the network.
The fix
The bfcache makes eligible pages available instantly: it’s more like showing a hidden page than “loading” a document in the traditional sense. Indeed, the event that can tell us if the document was restored from bfcache is called pageshow.[1]
If a pageshow event’s persisted property is true, the page was restored. So we:
- check for
persisted - remove previous of evidence of Munchkin
- reinject the Munchkin bootstrap (which loads the library)
Simple stuff:
window.addEventListener("pageshow", function(e){
if(e.persisted){
delete window.Munchkin;
delete window.MunchkinTracker;
const munchkinBootstrap = document.createElement("script");
munchkinBootstrap.src = "//munchkin.marketo.net/munchkin-beta.js";
const munchkinInit = {
handleEvent(e){
Munchkin.init("123-ABC-456"); // plus Munchkin options
}
};
munchkinBootstrap.addEventListener("load", munchkinInit, { once: true });
document.head.append(munchkinBootstrap);
}
});Now, a quick back-and-forth doesn’t lose any hits:
Notes
[1] Familiar events like DOMContentLoaded and window load don’t fire again when bfcache is used, which makes sense — it would be catastrophic to re-fire those events on a page that was merely temporarily hidden.