Stale-while-revalidate (SWR) caching strategies provide faster feedback to the user of web applications, while still allowing eventual consistency. Faster feedback reduces the necessity to show spinners and may result in better-perceived user experience.
Jeff Posnick explained in a blog entry the rationale behind the stale-while-revalidate
caching strategy:
stale-while-revalidate
helps developers balance between immediacy—loading cached content right away—and freshness—ensuring updates to the cached content are used in the future.
stale-while-revalidate
and the max-age
parameters can be set in the Cache-Control
of the HTTP response header. A cached response to an HTTP request that is older than max-age
(expressed in seconds) is considered stale. In case of a stale response, if the age of the cached response is within the window of time covered by the stale-while-revalidate
setting, the stale response is returned in parallel to a revalidation request being performed. The response to the revalidation request replaces the stale response in the cache. If the cached response is older than the window of time covered by the stale-while-revalidate
setting, the browser will instead retrieve a response directly from the network and populate the cache with that response.
Google, which improved the speed and performance of Google’s display ads with stale-while-revalidate strategies, explained:
This same technique can be applied to any other scenario where loading scripts as quickly as possible is more important than loading the freshest code.
Support for setting stale-while-revalidate
alongside max-age
in your Cache-Control
response header is available in Chrome 75 and Firefox 68. Browsers without support for stale-while-revalidate
silently ignore that configuration value and use max-age
.
Developers may also usestale-while-revalidate
strategies in single-page applications that make use of dynamic APIs. In such applications, oftentimes a large part of the application state comes from remotely stored data (the source of truth). As that remote data may be changed by other actors, fetching it anew on each request guarantees to always return the freshest data available. Stale-while-revalidate
strategies substitute the requirement to always have the latest data for that of having the latest data eventually.
The mechanism works in single-page applications in a similar way as in HTTP requests. The application sends a request to the API server endpoint for the first time, caches and returns the resulting response. The next time the application will make the same request, the cached response will be returned immediately, while simultaneously the request will proceed asynchronously. When the response is received, the cache is updated, with the appropriate changes to the UI taking place.
The stale-while-revalidate
strategy thus allows most of the time for instantaneous updates of the user interface, and eventual correctness of the displayed data since fresh response data is displayed as soon as it is available.
The Next.js React application framework provides developers with the SWR hook. The following code provides an example of usage:
// Custom React hook using the useSWR hook to fetch remote data about a user
function useUser (id) {
const { data, error } = useSWR(`/api/user/${id}`, fetcher)
return {
user: data,
isLoading: !error && !data,
isError: error
}
}
// page component
function Page() {
return <div>
<Navbar />
<Content />
</div>
}
// child components
function Navbar () {
return <div>
...
<Avatar />
</div>
}
function Content () {
const { user, isLoading } = useUser()
if (isLoading) return <Spinner />
return <h1>Welcome back, {user.name}</h1>
}
function Avatar () {
const { user, isLoading } = useUser()
if (isLoading) return <Spinner />
return <img src={user.avatar} alt={user.name} />
}
The previous code showcases a useUser
hook that leverages the useSWR
hook to fetch remote user data. Two components (Content
and Avatar
) call the useUser
hook, with only one request sent to the API, because they use the same SWR key (that used by useUser
) and the request is deduped, cached, and shared automatically.
The useSWR
hook can additionally be customized to revalidate its cache when a component is mounted (revalidateOnMount
), when the window gets focused (revalidateOnFocus
), when the browser regains a network connection (revalidateOnReconnect
), at a constant interval (refreshInterval
), and at other configurable times (cf. documentation). React’s reactive binding ensures that when the cache is updated with the fresh response, the UI will be updated to reflect the new data.
Tim Raderschad presented in a talk at Svelte Summit 2020 the implementation in Svelte of a similar API cache validation strategy. With Svelte’s usual succinctness, the implementation is only 20 lines and leverages Svelte’s stores:
// ./fetcher file
import { writable } from 'svelte/store'
const cache = new Map();
export function getData(url) {
const store = writable(new Promise(() => {}));
if(cache.has(url)){
store.set(Promise.resolve(cache.get(url)));
}
const load = async () => {
const response = await fetch(url);
const data = await response.json();
cache.set(url, data);
store.set(Promise.resolve(data));
}
load();
return store;
}
Any component can then import the previous function to fetch and cache data:
<script>
import Error from './Error.svelte'
import Spinner from './Spinner.svelte'
import { getData } from './fetcher'
const response = getData('https://0nzwp.sse.codesandbox.io/')
</script>
<h1>New Fetch</h1>
{#await $response}
<Spinner />
{:then data}
<code>{new Date(data).toTimeString()}</code>
{:catch}
<Error />
{/await}
Svelte’s store binding ensures there again that store updates are propagated to the user interface.
It is worth noting that SWR caching strategies in single-page applications are best used when it is appropriate for the application to temporarily show stale data.