Learn Caching

Unpoly caches responses so that pages the user has already visited this session render instantly on their next visit. Cached content is revalidated with the server after rendering, so stale content is quickly replaced. Cached pages also remain accessible after a disconnect.

Which responses are cached

Caching is controlled by the { cache } render option or an [up-cache] attribute. When navigating, the { cache: 'auto' } option is already set by default. It caches all responses to GET requests, so following a link or submitting a GET form fills the cache without any setup.

A render pass that does not navigate, like a plain up.render() call, only caches when you ask for it:

up.render({ target: '.content', url: '/stories/5', cache: 'auto' }) // mark: cache: 'auto'

Which requests 'auto' caches is decided by up.network.config.autoCache. By default it caches requests with a safe HTTP method like GET. You can configure this default.

To force caching regardless of HTTP method, pass { cache: true } or set [up-cache="true"].

Preloading a link also writes its response to the cache.

Error responses are not cached. They also evict an earlier cached response for the same URL. When a request for the same URL is already loading, a second request waits for it instead of making another network request.

Revalidation

Cache entries are only considered fresh for 15 seconds. When Unpoly renders older content from the cache, it reloads the fragment from the server right after. This process is called cache revalidation.

When re-visiting pages, Unpoly often renders twice:

  1. An initial render pass from the cache, which may be expired
  2. A second render pass from the server, which is always fresh

The user sees the cached version instantly and the fresh version a moment later. This has some benefits:

  • The user always ends up with fresh content. If another user has added an item to a cached list, the user sees that new item after revalidation.
  • Cache entries can be kept for a long time, allowing instant navigation for 90 minutes, even when offline or on a flaky connection.
  • Nothing needs to clear the cache after a form submission. Cache entries are only marked as expired.

Note

Revalidation only happens after expired content was rendered into the page. No revalidation occurs when expired cache entries are accessed without rendering, e.g. when preloading a cached URL.

When nothing changed

Your server-side app is not required to re-render a page when the cached content is still current.

By supporting conditional requests, it can answer a revalidation request with an empty 304 Not Modified response. Unpoly then keeps the fragment it has already rendered.

Controlling revalidation

Revalidation is controlled by the { revalidate } render option or an [up-revalidate] attribute. When navigating, { revalidate: 'auto' } is the default. It revalidates only expired cache entries. You can configure both the expiry age and the rule:

up.network.config.cacheExpireAge = 20_000 // expire after 20 seconds

let defaultAutoRevalidate = up.fragment.config.autoRevalidate
up.fragment.config.autoRevalidate = function(response) {
  return defaultAutoRevalidate(response) && response.url !== '/dashboard'
}

To revalidate regardless of a cache entry's age, pass { revalidate: true }.

When not navigating, pass { revalidate: 'auto' } or set an [up-revalidate="auto"] attribute to revalidate expired entries.

Disabling revalidation

Navigation is the only moment when Unpoly revalidates by default. To disable revalidation while navigating:

up.fragment.config.navigateOptions.revalidate = false

To keep the default, but disable revalidation for an individual link, set an [up-revalidate="false"] attribute:

<a href="/" up-revalidate="false">Start page</a> <!-- mark: up-revalidate="false" -->

To keep the default, but disable revalidation for a function call that would otherwise navigate, pass a { revalidate: false } option:

up.follow(link, { cache: true, revalidate: false })

Expiration

Cached content expires after 15 seconds. This can be configured in up.network.config.cacheExpireAge. The configured age should at least cover the typical time between preloading a link and following it.

Expired content stays in the cache, but triggers revalidation when rendered. Expired pages also remain accessible after a connection loss.

Expiring content after an interaction

GET requests don't expire any content. When the user makes a non-GET request (usually a form submission with POST), the entire cache is expired as soon as the request is sent. A non-GET request has probably changed data on the server, so all cache entries should be revalidated before they are shown again.

To change what a request expires, use any of the following. A value of false expires nothing:

To expire additional entries later:

  • Send an X-Up-Expire-Cache response header from the server. It cannot undo the expiration that happened when the request was sent.
  • Expire cache entries from JavaScript with up.cache.expire()

Each of these accepts a URL pattern to expire only matching entries:

up.cache.expire('/users/*')

Disabling caching

You can disable the caching mechanism globally or selectively.

Without caching, every request and render pass hits the network. Responses are not cached either, unless there is an existing cache entry that can be updated with a fresher response.

Disabling the cache globally

To turn off caching entirely, e.g. while debugging, disable the cache:

up.network.config.cacheEnabled = false

No response is read from or written to the cache. Preloading is disabled as well.

Disabling the cache for selected routes

To keep the navigation default, but disable auto-caching for some URLs, configure up.network.config.autoCache:

let defaultAutoCache = up.network.config.autoCache
up.network.config.autoCache = function(request) {
  return defaultAutoCache(request) && !request.url.endsWith('/edit')
}

To keep the default, but disable caching for an individual link, set an [up-cache="false"] attribute:

<a href="/stock-charts" up-cache="false">View latest prices</a> <!-- mark: up-cache="false" -->

Disabling the cache for a function call

To keep the default, but disable caching for a function call that would otherwise navigate, pass a { cache: false } option:

up.follow(link, { cache: false })

Eviction

Instead of expiring content you may also evict content to erase it from the cache.

In practice you will often prefer expiration over eviction. Expired content remains available during a connection loss and for instant navigation, while revalidation ensures the user always ends up with a fresh revision. Evicted content is gone from the cache entirely, and a new network request is needed to access it again.

Evicting content after an interaction

Eviction is the right choice when it is not acceptable for the user to see even a brief flash of stale content before revalidation finishes. By default no request evicts content. The controls mirror those for expiration:

Each of these accepts a URL pattern to evict only matching entries. The configuration, option and attribute take effect when the request is sent. The response header takes effect when the response is received.

Capping memory usage

To limit the memory required to hold its cache, Unpoly evicts cached content in two ways:

Reacting to revalidation in code

Preventing rendering of revalidation responses

To discard revalidated HTML after the server has responded, you may call event.skip() on an up:fragment:loaded event with a { revalidating: true } property. This gives you a chance to inspect the response or DOM state right before a fragment would be inserted:

up.on('up:fragment:loaded', function(event) {
  // Don't insert fresh content if the user has started a video
  // after the expired content was rendered.
  let video = event.request.fragment.querySelector('video')

  if (event.revalidating && video && !video.paused) { // mark: event.revalidating
    // Finish the render pass with no changes.
    event.skip()
  }
})

See skipping a loaded response for more details and examples.

Detecting revalidation from a compiler

Compilers with side effects may want to behave differently when the compiled element is being reloaded for the purpose of cache revalidation.

To detect revalidation, compilers may accept a third argument with information about the current render pass. In the example below a compiler wants to track a page view in a web analytics tool:

up.compiler('[track-page-view]', function(element, data, meta) { // mark: meta
  // Don't track duplicate page views if we just reloaded for cache revalidation.
  if (!meta.revalidating) {
    // Send an event to our web analytics tool.
    trackPageView(meta.layer.location)
  }
})

Caching optimized responses

Servers may inspect request headers to optimize responses, e.g. by omitting a navigation bar that is not targeted. Request headers that influenced a response should be listed in a Vary response header, so Unpoly can partition its cache by header value.

Matching cache entries must then have the same values for all listed headers. See Caching optimized responses for details, and How cache entries are matched for tables of hits and misses.

Caching after redirects

  • When a request GET /foo redirects to GET /bar, the response to /bar will be cached for both GET /foo and GET /bar.
  • For technical reasons Unpoly cannot read from the cache when a request to an uncached URL redirects to a cached URL. For example, when a form submission makes a request to POST /action, and the response redirects to GET /path, the browser will make a fresh request to GET /path even if GET /path was cached before.
  • For technical reasons Unpoly cannot detect redirects to the same URL, but using a different method. For example, when a request to POST /users redirects to GET /users. You can address this by including an X-Up-Method header in your responses.