Pagefold

Caching and history

How visited pages are stored, how to limit the cache, and how the back button behaves.

The cache

Pagefold keeps the HTML of every page it fetches in memory, keyed by its path and query. Visiting it again is instant and makes no request. Only GET requests that were not redirected are stored.

const pagefold = new Pagefold({
  cacheMax: 30,       // keep at most 30 pages, dropping the least recently used
  cacheTtl: 60_000,   // forget pages after one minute
})

Without these options the cache is unlimited and pages never expire.

This is how cacheMax: 3 behaves. Reading a page counts as using it, so it moves to the most recent end. When a fourth page arrives, the one at the other end is dropped:

← least recently usedmost recent →
Visit /a, /b, /c
/a/b/c
Visit /a again
/b/c/a
Visit /d (full)
/b/c/a/d
Green is a page just stored, blue one just used again, crossed out is the one dropped. /b was the least recently used, so it goes first.

Why not keep everything?

Instant revisits are tempting, so it is easy to think “the bigger the cache the better”. It is not free:

ProblemWhat happens
MemoryEach entry is the whole HTML of the page. 200 pages of 100 KB are about 20 MB held until the tab closes, and more if your pages are heavy. Phones and old laptops feel it first
Stale contentA cached page is a photograph. Prices, stock, “3 new comments” or a logged-in header stay as they were when it was fetched, and the visitor sees the old version for as long as it stays cached
Wrong state after login or logoutA page cached while logged out keeps showing the “Sign in” button after the visitor signs in
Hidden bugs“It works after a reload” is the classic sign of a stale cache, and it is hard to reproduce because it depends on the path the visitor took

The cache lives in memory, so a full page reload always starts empty. That caps the damage, but a long session can go a long way.

cacheTtl and cacheMax do different jobs. cacheTtl is about freshness: an expired page is ignored the next time it is requested, but it is only removed from memory at that moment, so a page nobody visits again stays in memory. cacheMax is about memory: it is the only one that puts a ceiling on how many pages are held. Use both on a site that changes.

How to choose:

Your siteSuggested settings
Mostly static: portfolio, docs, blogcacheMax: 50, no cacheTtl
Content that changes during the day: news, shop, listingscacheMax: 30, cacheTtl: 60_000 (one minute)
Personalised or live data: dashboards, accountscache: false to turn it off everywhere, or a short cacheTtl plus pagefold.cache.clear() when the user changes

These are starting points, not rules. Start small and raise the numbers only if you can see revisits that should have been instant.

Working with the cache directly

pagefold.cache is the same store the library uses, so you can read it and change it. You need this less often than you might think, because the options above cover most sites. It is for the moments when your code knows something Pagefold cannot: that the visitor changed something, or that a page is about to be needed.

MethodWhat it doesWhen it is useful
has(url)true if the page is storedDecide what to show: skip a loading hint when the page is already there
get(url){ url, html } or undefinedRead a stored page without visiting it, for example to take its <title> for a tooltip
delete(url)Forget one pageYou changed what that page shows
prune(fn)Forget every page for which fn(url, page) returns trueYou changed a whole area of the site
allEvery stored page, as a Map copy, oldest firstInspect or export what is cached. Changing the copy does not change the cache
clear()Forget everythingThe visitor’s identity or language changed

Examples

You changed data, so the page is out of date. After adding an item to the cart, the cached /cart page is wrong. Forget it and the next visit fetches a fresh one:

async function addToCart(id) {
  await fetch('/api/cart', { method: 'POST', body: JSON.stringify({ id }) })
  pagefold.cache.delete('/cart')
}

An area of the site changed. After editing a profile, every page under /account may show the old name:

pagefold.cache.prune((url) => url.startsWith('/account'))

Everything is different now. Signing in, signing out or switching language changes the header of every page, so drop them all:

logoutButton.addEventListener('click', () => pagefold.cache.clear())

Show something only when the page needs fetching. If the page is stored the visit is instant, so a loading hint would only flicker:

if (!pagefold.cache.has('/reports')) showSpinner()
await pagefold.navigate('/reports')

If you only need one visit to be fresh, you do not need any of this. Skip the cache for that visit instead:

To skip it for one visit: pagefold.navigate('/news', { cache: false }), or { cache: { read: false } } to refetch but still store the result. Turn it off for everything with cache: false.

History

Each visit adds an entry to the browser history, so the back and forward buttons work. Pagefold stores its own state in each entry and ignores history entries it did not create.

On back and forward visits:

  • the content is swapped without animation, because the browser itself restores the scroll position;
  • visit.history.popstate is true and visit.history.direction is 'backwards' or 'forwards';
  • the is-popstate class is present.

To animate them, set animateHistoryBrowsing: true. Pagefold then takes over scroll restoration and scrolls to the top.

Replacing instead of adding

pagefold.navigate('/step-2', { history: 'replace' })

Or data-pagefold-history="replace" on the link.