React Storefront
|

Caching

Effective use of caching in the browser and at the edge is essential to providing the fastest experience for your users. React Storefront allows you to control both client and edge caching from a single place: your router definition.

This guide gives you everything you need to know to cache responses effectively using React Storefront.

Making a Route Cacheable

You can make any route cacheable by adding a cache handler to the route.

For example:

// src/routes.js

import { Router, fromServer, cache } from 'react-storefront/router'

new Router()
  .get('/',
    cache({ client: true, edge: { maxAgeSeconds: 300 }}) // cache in the service-worker and at the edge for up to 5 minutes
    fromServer('./home/home-handler')
  )

The cache handler takes an object with with two properties:

  • client: (Boolean) Set to true to cache responses on the client using the service worker. Set to false or omit to prevent caching on the client.
  • edge: (Object) Configures caching at the edge. If omitted, responses will not be cached at the edge
    • maxAgeSeconds (Integer) The time to live in seconds

Setting the TTL a fromServer handler

You can also set maxAgeSeconds in a fromServer handler by calling response.cacheAtEdge({ maxAgeSeconds: (time) }). Here's an example:

function myHandler(params, request, response) {
  response.cacheAtEdge({ maxAgeSeconds: time })

  return {
    title: 'Hello World'
  }
}

Caching on the Client

All caching on the client is managed by the service worker that react-storefront automatically generates when your app is built.

In additon to specifying cache({ client: true }) on each route, you can configure client-side caching parameters for all routes using router.configureClientCache(options):

// src/routes.js

import { Router } from 'react-storefront/router'

new Router().configureClientCache({
  maxEntries: 1000,
  maxAgeSeconds: 300
})

The configureClientCache function takes an object with the following properties:

PropertyDefaultDescription
maxEntries200The maximum number of entries stored in the browser's cache
maxAgeSeconds3600The time to live of each entry

Caching at the Edge

When you specify a cache handler with, for example, { edge: { maxAgeSeconds: 300 }}, React storefront adds a cache-control: no-cache, s-maxage: 300 header to the response. This instructs the hosting platform to cache the response for the configured number of seconds.

Caching Multiple Variants of the Same URL

Default cache key

The default cache key in Moovweb XDN consists of the following request parameters:

  • Version of the project deployment
  • Domain on which the request was made
  • Path of the request
  • Backend to which the request was routed
  • Moovweb mode cookie if one exists
  • TLS flag

Implications are as following:

  • Every project deployment will have a separate cache space
  • Every domain in the project, if your project serves more than one domain, will have a separate cache space
  • Every path in the project will have a separate cache space
  • Every backend will have a separate cache space (so even in the same version a request made to say desktop backend will be a separate cache entry from the one made to say mobile backend)
  • Every mode in a project will have a separate cache space
  • HTTPS requests will have a different cache space from HTTP (although since XDN doesn't allow HTTP traffic in practice this is relevant only for 301s redirecting from HTTP to HTTPS)

Custom cache keys

You can configure the Moovweb XDN to store multiple variants for any given route by specifying a custom cache key. This is essential when you need to serve multiple languages, currencies, or shipping locations from the same domain.

Here is an example which uses a cookie called "currency" to split the cache:

import {
  Router,
  fromClient,
  fromServer,
  cache,
  createCustomCacheKey
} from 'react-storefront/router'

new Router().get(
  '/p/:id',
  cache({
    edge: {
      maxAgeSeconds: 3600,
      key: createCustomCacheKey().addCookie('currency') // split the cache based on the currency cookie
    }
  }),
  fromClient({ page: 'Product' }),
  fromServer('./product/product-handler')
)

In this scenario, the Moovweb XDN will store a separate response in the cache for each value of the currency cookie. For example, A user with a currency cookie of "USD" will get a different cached response from a user with a currency cookie of "EUR".

Grouping Cookies

You can return a single cached response for multiple values of a cookie by passing an optional partitioning callback to addCookie(). Here we create two partitions: one called "na" representing North America, consisting of "us" and "ca", and one for all others.

In this example users with a location cookie with value of "us" or "ca" will share one response, while all other users will share a different response:

createCustomCacheKey().addCookie('location', cookie => {
  cookie.partition('na').byPattern('^(us|ca)$')
  cookie.partition('others').byPattern('.*')
})

Clearing the Client Cache when Cookies Change

If you're using cookies to cache multiple variants of a page at the edge, and those cookies can be set by the user (for example, by allowing the user to change their currency preference), it is important to clear the client cache when those cookies change so that stale responses are discarded. This can be done using clearClientCache from react-storefront/cache:

import React, { useState } from 'react'
import { clearClientCache } from 'react-storefront/cache'

function CurrencySelection() {
  const [value, setValue] = useState('USD')

  const onChange = e => {
    // update the cookie
    document.cookie = `currency=${e.target.value}`

    // since we're splitting the cache by currency, we need to clear the client cache
    clearClientCache()

    // update local component state
    setValue(e.target.value)
  }

  return (
    <select onChange={onChange} value={value}>
      <option value="USD">USD</option>
      <option value="EUR">EUR</option>
    </select>
  )
}

Splitting the Cache by Headers

You can also split the cache by request headers and optionally group values by pattern:

new Router().get(
  '/p/:id',
  cache({
    edge: {
      maxAgeSeconds: 3600,
      key: createCustomCacheKey()
        .addHeader('x-moov-xdn-device-is-bot')
        .addHeader('x-moov-xdn-geo-country-code', header => {
          header.partition('na').byPattern('^(us|ca)$')
          header.partition('others').byPattern('.*')
        }) // split the cache based on the location header
    }
  }),
  fromClient({ page: 'Product' }),
  fromServer('./product/product-handler')
)

Ignoring Query Parameters When Caching

URLs often contain unique tracking codes in order to track customer engagement. A common example is embedding a unique user id in links that are sent out as part of an email ad campaign. Consider the following URL:

https://mystore.com/products/red-shirt?uid=1234567

Here the uid query parameter is used to track that the user has clicked on the link. Because the value of this query parameter is different for every user, it will never be served from the cache. Therefore, the initial landing on the site will be quite slow for all users. By configuring your routes to ignore the uid query parameter, we can serve this page to users from the cache:

new Router().get(
  '/p/:id',
  cache({
    edge: {
      maxAgeSeconds: 3600,
      key: createCustomCacheKey().excludeQueryParameters('uid') // ignore the uid query paramater
    }
  }),
  fromClient({ page: 'Product' }),
  fromServer('./product/product-handler')
)

You can also ignore all query parameters:

createCustomCacheKey().excludeAllQueryParameters() // ignore all query parameters

Or, exclude all but specific query parameters:

createCustomCacheKey().excludeAllQueryParametersExcept('search', 'style') // ignore all query parameters except "search" and "style"

Note that when you exclude unique tracking query parameters from the cache key, you need to report the user interaction via JavaScript running in the browser, after the app mounts, not on the server.

Surrogate Keys

React Storefront allows you to assign one or more surrogate keys to each route. A surrogate is a value that you can use to clear a group of responses from the cache using the Moovweb Control Center.

Here's an example of how you can assign the same surrogate key to multiple routes:

new Router()
  .get(
    '/categories/:id',
    cache({
      edge: {
        surrogateKey: (params, request) => {
          return 'product-catalog'
        }
      }
    })
  )
  .get(
    '/products/:id',
    cache({
      edge: {
        surrogateKey: (params, request) => {
          return 'product-catalog'
        }
      }
    })
  )

Here both category and product routes are assigned the 'product-catalog' surrogate key. Assigning this surrogate key would allow you to clear only these two routes from the cache while leaving the rest of the cached pages intact using the Moovweb Control Center:

clearing a surrogate key using Moovweb Control Center

You can also clear items with a particular surrogate key from the cache from the command line using moovsdk:

$ moovsdk cacheclear --surrogate-key product-catalog

All routes are automatically given the handler path as a surrogate key. So for example, if you have:

new Router().get(
  '/products/:id',
  cache({ edge: { maxAgeSeconds: 1000 } }),
  fromClient({ page: 'Product' }),
  fromServer('./products/product-handler')
)

You can remove all product pages from the cache using the Moovweb Control Center...

clearing a surrogate key by handler path using Moovweb Control Center

... or moovsdk ...

$ moovsdk cacheclear --surrogate-key ./products/product-handler

Assigning Multiple Surrogate Keys to a Response

You can assign multiple surrogate keys to a response by separating them with a space:

.get(
  '/products/:id',
  cache({
    edge: {
      surrogateKey: (params, request) => {
        return `all-products product-${params.id}`
      }
    }
  })
)

In this example, you could clear a URL of /products/1 from the cache using:

$ moovsdk cacheclear --surrogate-key product-1

Or you could clear all products using:

$ moovsdk cacheclear --surrogate-key all-products

In order to prevent responses with set-cookie headers from being cache (and thus possibly leaking user-specific information like a session id to multiple users), React Storefront automatically removes all set-cookie headers when your route is configured to cache at the edge.

Static Assets

Your JS bundles are far-future cached and contain a cache-busting hash in the filename. Therefore they are safe to cache in the browser (even when service workers are not supported) and on the downstream CDN.

All other static assets will not have a cache-control header. They will be cached based on the CDN and browsers default behavior. If hosting on the Moovweb XDN, all static assets are far-future cached at the edge.

Moovweb XDN

Moovweb's XDN platform is automatically configured to respect this header. If you're hosting on another platform, you'll probably need to configure it to do so.

FAQ

How do I properly clear just the homepage of my site?

  1. Go to the Project Settings in Moovweb Control Center for your project and click on the Cache tab.
  2. Select By specific path in the options menu.
  3. Specify / as the path as shown in the image below: Clearing the homepage cache using Moovweb Control Center
  4. Press the appropriate button to clear cache:
    • To clear the homepage for mobile devices, click on the Flush Mobile Caches button.
    • To clear the homepage for desktop devices, click on the Flush Desktop Cache button.
    • To clear the homepage for all devices or if no other buttons are not present, click on the Flush All Caches button.

Note that specifying a path of / will clear the cache for when the homepage is requested with a trailing / (e.g. https://www.site.com/) and also for when the homepage is requested without a trailing / (e.g. https://www.site.com).

How do I know if a response came from the edge cache?

You can use the x-moov-t response header. If you see vc=hit in the x-moov-t response header. A value of vc=cached indicates a miss but the result has been cached for the next request. All other values indicate a cache miss.

There is also a x-moov-cache-reason that specifies whether or not the result was cached and why. For example:

x-moov-cache-reason: cached: cache-control: s-maxage found in response

There is no cache layer during local development, how do I know that a response will be cached when I deploy to Moov XDN?

If you see a cache-control response header with an s-maxage value, Moov XDN will cache it.

Why don't I see an s-maxage value when my app is deployed on Moov XDN?

Moov XDN strips this value off before it sends the response. This helps ensure that the downstream CDN won't cache the response. This is important to prevent stale responses that would otherwise occur after you deploy an update to your application.

How do I clear the cache?

Moov XDN automatically clears the edge cache when you deploy an update to your application.

You can also clear it manually at any time using the Moovweb Control Center. Navigate to your project, then select Project Settings => Project Admin => Clear Hosts Cache.

What if I add ?moov_debug=true to my URL?

You will not receive a cached response.