Unpoly caches responses, allowing instant access to pages that the user has already visited this session.
To ensure that the user never sees stale content, cached content is revalidated with the server.
Cached pages also remain accessible after a disconnect.
You can enable caching with { cache: 'auto' }
, which caches all responses to GET requests.
You can configure this default:
up.network.config.autoCache = (request) => request.method === 'GET'
When navigating the { cache: 'auto' }
option is already set by default.
To force caching regardless of HTTP method, pass { cache: true }
.
Navigation is the only moment when Unpoly caches by default.
You can disable caching while navigating like so:
up.fragment.config.navigateOptions.cache = false
If you want 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) {
defaultAutoCache(request) && !request.url.endsWith('/edit')
}
If you want to keep the default, but disable caching for an individual link, use an [up-cache=false]
attribute:
<a href="/stock-charts" up-cache="false">View latest prices</a>
If you want to keep the default, but disable caching for a function call that would otherwise navigate,
pass an { cache: false }
option:
up.follow(link, { cache: false })
Cache entries are only considered fresh for 15 seconds. When rendering older cache content, Unpoly automatically reloads the fragment to ensure that the user never sees expired content. This process is called cache revalidation.
When re-visiting pages, Unpoly often renders twice:
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).
You can enable revalidation with { revalidate: 'auto' }
, which revalidates expired cache entries. You can configure this default:
up.network.config.cacheExpireAge = 20_000 // expire after 20 seconds
up.fragment.config.autoRevalidate = (response) => response.expired
To force revalidation regardless of cache age, pass { revalidate: true }
.
Navigation is the only moment when Unpoly revalidates by default.
You can disable cache revalidation while navigating like so:
up.fragment.config.navigateOptions.revalidate = false
If you want to keep the navigation default, but disable revalidation for some responses, configure up.fragment.config.autoRevalidate
:
up.fragment.config.autoRevalidate = (response) => response.expired && response.url != '/dashboard'
If you want to keep the default, but disable caching for an individual link, use an [up-revalidate=false]
attribute:
<a href="/" up-revalidate="false">Start page</a>
If you want to keep the default, but disable revalidation for a function call that would otherwise navigate,
pass an { revalidate: false }
option:
up.follow(link, { cache: true, revalidate: false })
Your server-side app is not required to re-render a request if there are no changes to the cached content.
By supporting conditional HTTP requests you can quickly produce an empty revalidation response for unchanged content.
To discard revalidated HTML after the server has responded, you may prevent the
up:fragment:loaded
event when it has an { 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.
if (event.revalidating && !event.request.fragment.querySelector('video')?.paused) {
// Finish the render pass with no changes.
event.skip()
}
})
See skipping unnecessary rendering for more details and examples.
Compilers with side effects may occasionally 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-phrase "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)
}
})
Cached content automatically expires after 15 seconds. This can be configured in up.network.config.cacheExpireAge
. The configured age should at least cover the average time between preloading and following a link.
After expiring, cached content is kept in the cache, but will trigger revalidation when used. Expired pages also remain accessible after a connection loss.
GET
requests don't expire any content by default. When the user makes a non-GET
request (usually a form submission with POST
), the entire cache is expired. The assumption here is that a non-GET request will change data on the server, so all cache entries should be revalidated.
There are multiple ways to override this behavior:
up.network.config.expireCache
{ expireCache }
option to the rendering function[up-expire-cache]
attribute on a link or formX-Up-Expire-Cache
response header from the serverup.cache.expire()
functionInstead 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 sees a fresh revision. Evicted content on the other hand is gone from the cache entirely, and a new network request is needed to access it again.
One use case for eviction is when it is not acceptable for the user to see a brief flash of stale content before revalidation finishes. You can do so in multiple ways:
up.network.config.evictCache
{ evictCache }
option to the rendering function[up-evict-cache]
attribute on a link or formX-Up-Evict-Cache
response header from the serverup.cache.evict()
functionTo limit the memory required to hold its cache, Unpoly evicts cached content in the following ways:
up.network.config.cacheEvictAge
.up.network.config.cacheSize
.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.
This tells Unpoly to partition its cache for that URL so that each
request header value gets a separate cache entries.
The user makes a request to /sitemap
in order to updates a fragment .menu
.
Unpoly makes a request like this:
GET /sitemap HTTP/1.1
X-Up-Target: .menu
The server may choose to optimize its response by only render only the HTML for
the .menu
fragment. It responds with the HTTP seen below. Note that it includes a Vary
header
indicating that the X-Up-Target
header has influenced the response body:
Vary: X-Up-Target
<div class="menu">...</div>
After observing the Vary: X-Up-Target
header, Unpoly will partition cache entries to /sitemap
by X-Up-Target
value.
That means a request targeting .menu
is no longer a cache hit for a request targeting a different selector.
By default cached responses will match all requests to the same URL.
When a response has a Vary
header, matching requests must additionally have the same values for all listed headers:
🠦 X-Up-Target: .foo 🠤 Vary: X-Up-Target
|
|
---|---|
🠦 X-Up-Target: .foo
|
✔️ cache hit |
🠦 X-Up-Target: .bar
|
❌ cache miss |
🠦 No X-Up-Target
|
❌ cache miss |
When a response has no Vary
header, that response is a cache hit for all requests to the URL, regardless of target:
🠦 X-Up-Target: .foo 🠤 No Vary
|
|
---|---|
🠦 X-Up-Target: .foo
|
✔️ cache hit |
🠦 X-Up-Target: .bar
|
✔️ cache hit |
🠦 No X-Up-Target
|
✔️ cache hit |
Requests can target multiple fragments by separating selectors
with a comma. If the server replies with Vary: X-Up-Target
, that response is a cache hit for each individual selector:
🠦 X-Up-Target: .foo, .bar 🠤 Vary: X-Up-Target
|
|
---|---|
🠦 X-Up-Target: .foo
|
✔️ cache hit |
🠦 X-Up-Target: .bar
|
✔️ cache hit |
🠦 X-Up-Target: .foo, .bar
|
✔️ cache hit |
🠦 X-Up-Target: .bar, .foo
|
✔️ cache hit |
🠦 X-Up-Target: .baz
|
❌ cache miss |
🠦 X-Up-Target: .foo, .baz
|
❌ cache miss |
🠦 No X-Up-Target
|
❌ cache miss |
GET /foo
redirects to GET /bar
, the response to /bar
will be cached for both GET /foo/
and GET /bar
.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.POST /users
redirects to GET /users
. You can address this by including an X-Up-Method
header in your responses.