DEV Community

Daniel Pertu
Daniel Pertu

Posted on

The back button costs nothing on 70 of our pages and a server render on the two that take money

Seventy of PubTrivia's 72 public pages are prerendered. The homepage and the pricing page are the two that render per request, because both read the visitor's country to choose a display currency, and both therefore answer cache-control: private, no-cache, no-store.

I knew what that meant for the CDN. I had not thought about what it means for the back button, which is the single most used control in a browser. It turns out no-store is one of the things that keeps a page out of Chrome's back/forward cache, and Chrome will tell you so by name.

The measurement

Over CDP: enable the Page domain, load page A, load page B, then go back with Page.navigateToHistoryEntry and listen for Page.backForwardCacheNotUsed. If that event does not fire, the document was restored. I also set a variable on window before leaving, listened for pageshow, and counted the network requests the back navigation caused.

Going back to a prerendered page:

back to /faq:                    requests during back = 0, document responses = []
back to /guides/pub-quiz-format: requests during back = 0, document responses = []
Enter fullscreen mode Exit fullscreen mode

Zero. No document request, no chunks, nothing. pageshow fired with persisted: true, window.__mark was still the string I had set before navigating away, performance.now() continued from where it left off, and the navigation timeline still reported navigate rather than back_forward, because it is the same navigation it always was. The document was frozen whole and thawed.

Going back to either of the two per-request pages:

back to /pricing: requests during back = 45, servedFromCache = 24, document = 200 MISS age:0
back to /:        requests during back = 50, servedFromCache = 29, document = 200 MISS age:0
Enter fullscreen mode Exit fullscreen mode

A fresh document, window.__mark gone, performance.now() restarted, navigation type back_forward, and a MISS on the document, which means a Vercel function ran and rendered the page again. Chrome's reasons, verbatim from the protocol:

Circumstantial: MainResourceHasCacheControlNoStore
Circumstantial: JsNetworkRequestReceivedCacheControlNoStoreResource
Circumstantial: BrowsingInstanceNotSwapped
Enter fullscreen mode Exit fullscreen mode

The first one is the real one. The second is the same fact noticed again at the subresource level. Both of them trace back to one line in each of two files, export const dynamic = "force-dynamic", which exists so that a visitor in Sweden sees kronor.

What is actually lost, and what is not

My first draft of this post said the scroll position was lost. That was wrong, and the way it was wrong is worth recording.

Scroll restoration is a separate mechanism from the back/forward cache. Chrome stores the scroll offset in the history entry, so even on a document it has to rebuild from scratch it puts you back where you were. I measured 900 pixels before leaving /pricing and 900 pixels a second after coming back, on a document that was demonstrably rebuilt.

The first measurement that said otherwise had two faults, both mine. I read scrollY too soon after scrollTo(), and this site sets html { scroll-behavior: smooth }, so an immediate read reports a position the page is still travelling towards rather than the one it will land on. Then the history entry recorded the stale offset. If you measure scrolling in a browser, wait for it, and read the value twice.

So the user-visible damage from missing the back/forward cache is not a jump to the top of the page. It is this:

  • 45 to 50 requests, roughly half of which do hit the HTTP cache, but the document never can, because no-store
  • a serverless function invocation on every back press, on the two pages a visitor bounces back to most while comparing plans
  • every bit of client state on the page thrown away, which on the pricing page includes which plan card the visitor had focused
  • the paint is a fresh render rather than a restore, which is the difference between a frame and a round trip to Washington, where our functions run

Is it fixable

Chrome has been working on allowing no-store pages into the back/forward cache, with eviction if the cookies change while the page is frozen. That would help us for free one day, and it is not something to plan around.

The real fix is not to be no-store. The currency decision could move to the client, or the pricing page could be prerendered per currency, both of which have their own costs that an earlier post went through and neither of which is free. What has changed today is that the trade now has a number attached: two pages out of 72 cannot be restored, and each unrestored back press is one server render.

Everything else on the site, including every guide, every comparison and every free tool, comes back for nothing.

Check it yourself, no CDP required

  1. Open pub-trivia.app/faq, open the console, and type window.x = 42.
  2. Click any link, then press the browser's back button.
  3. Type window.x. It is still 42: same document, restored from the back/forward cache.

Now do the same thing starting from pub-trivia.app/pricing. After the back button, window.x is undefined, and the Network panel shows the whole page coming down again with the document marked x-vercel-cache: MISS. Two pages on the site behave one way and seventy behave the other, and the only difference between them is one exported constant.

Top comments (1)

Collapse
 
jonasjavier profile image
Jonás Javier Encarnación •

The scroll correction is the paragraph I'll remember; I would have blamed the bfcache for that jump too. I hit the same fork with language instead of currency and went the prerender-per-variant way: no Accept-Language, every page lives under /en or /es and is static, and only / redirects, to /es when the visitor's language cookie says so, which is a cookie match in next.config rather than a render. For currency the signal is geo, not a cookie, so I'm not sure that trick carries over. Did you look at doing the country split as a redirect to /pricing/sek and keeping each one prerendered?