Maximizing Your Cache Hit Ratio

One of the best ways to improve the performance of your storefront is to maximize your cache hit ratio. Any request that can be fulfilled from the Managed Runtime’s CDN cache counts as a cache hit. With each cache hit, you speed up page loading for the user because you save the cost of rendering that page on the server side and making any requests to back-end systems and APIs.

This guide covers three different techniques for maximizing your cache hits: setting optimal cache lifetimes, conditional rendering, and filtering query strings. Let’s go through each technique in detail.

Setting Optimal Cache Lifetimes

The optimal amount of time to cache an HTTP request depends on what’s being requested. A request to load a content page that rarely changes can be safely cached for a long time. In contrast, a product listing page that’s frequently updated with new products might require a short cache lifetime, such as fifteen minutes. Whenever possible, choose long cache lifetimes in order to maximize the cache hit rate.

A cache-control header in the HTTP response determines the length of time that a response to a page request is stored in the CDN cache. The default cache lifetime for page responses in a PWA Kit project is 600 seconds (or 10 minutes) by default. You can customize the cache lifetime on a page-by-page basis, or you can customize it for all pages.

To learn more about caching in general, we recommend this article from Google: Prevent unnecessary network requests with the HTTP Cache.

Setting the Cache Lifetime for a Page

The getProps function of any page component allows you to customize the HTTP response using an object that is passed in called res. To set the cache lifetime for a page, use the set method of the res object to set a Cache-Control header and specify a value in seconds for the max-age directive.

For example, this ProductList component extends the cache lifetime from the default of 600 seconds to 900 seconds:

We don’t recommend that you set a cache-control header within the getProps function that belongs to the App special component defined in app/components/_app/index.jsx. The getProps functions for both the App component and the current page component are executed at the same time. This parallel execution leads to unpredictable results if you set the same response header in both functions.

Setting the Cache Lifetime for All Pages

You can set the cache lifetime for all pages in app/ssr.js. You can change the default cache lifetime by setting a new value for defaultCacheTimeSeconds. If you need more fine-grained control over the HTTP header, add an Express handler that sets a custom header, like this:

Testing the Response

You can test that your cache controls are present in the response headers by inspecting your network requests with Chrome’s DevTools in the Network tab. Alternatively, you can run the following curl command in your terminal, which outputs all response headers. Replace <URL> in the example command with the full URL that you want to test, including any required query strings.

Conditional Rendering

To ensure that a page is suitable for caching by the CDN, you must add conditional code to avoid rendering the following types of content on the server side:

  • Personalized content, such as a user’s name, the number of items in the cart, and preferred payment method. Personalized content is inappropriate for and irrelevant to anyone other than a single user. Caching responses for a single user doesn’t increase your cache hits.
  • Frequently changing content, such as a product’s price, remaining inventory, or sales promotions. This content is unsuitable for caching because there’s a risk of confusing the user when the page contains information that is already out-of-date.

Think of the page that is rendered on server side as a generic foundation for the client to build on. After quickly loading the server-side version of the page onto the user’s device, the browser takes over to render the personalized and frequently changing content.

To determine whether rendering is happening on the client side or server side, check for the presence of the window object, which is only present on the client side. The following example uses this technique to render a price only on the client side:

To preview the version of the page that is rendered on the server side in your browser, append ?mobify_server_only to the URL. This query parameter stops the hydration process so that the browser won’t take over rendering, leaving the page unchanged after server-side rendering.

Filtering Query Strings

Most storefront apps use the query string of a URL to store parameters and values that represent aspects of the app’s state. For example, when a user searches for “sweaters,” you can include the search term in the query string like this: ?search=sweaters. Query strings are also commonly used to track user actions. For example, we can append a unique query string to each link in an email to track interactions with it: user=juanita&source=email.

Not all query string parameters are relevant when it comes to caching. The Managed Runtime includes an edge function called the request processor that allows you to modify the query string of a request before it looks for cached responses. You can increase your cache hits by using the request processor to map similar URLs to the same cached response.

To customize the request processor, edit the processRequest function defined in app/request-processor.js.

The following example defines a request processor that filters out the parameters gclid and utm_campaign from the query string. These parameters are commonly associated with Google marketing campaigns and are only useful on the client side. To simplify working with query strings, it imports the QueryParameters class from the PWA Kit React SDK.

The full URL that is used to look up the corresponding object in the cache includes the version of the query string that is returned by the request processor. If no response is cached for that URL, this same modified version of the URL is passed to the Express app.

Beware of two gotchas when using this approach:

First, be sure that your app doesn’t rely on any filtered parameters for rendering! For example, if you filtered the search parameter from above, you’d have a hard time displaying the correct search results.

Second, be cautious when filtering parameters from requests that you expect to redirect. If your code can’t access a filtered parameter, that parameter can’t be used for the redirect either. Consider a request a home page component that redirects a request for www.example.com?lang=en to a locale-specific path like www.example.com/en. If you filter out the lang parameter, you can’t redirect to the right locale.

Consider this sequence:

  1. The request processor handles a request for www.example.com/?gclid=123.
  2. The request processor filters the gclid query string parameter.
  3. The request is forwarded to the application with the full URL www.example.com.
  4. The application returns a redirect to www.example.com/en.

Notice that on this last step, we’ve lost the original gclid parameter, so it won’t be available to the browser after the user is redirected. To work around this challenge, avoid filtering query strings of requests you expect to redirect.

Next Steps

Now you know how to improve your cache hits for page requests using a variety of techniques.

Read our guide to Proxying Requests to learn about caching API requests and other benefits of proxying.

To learn more about performance optimization, keep exploring the How-To Guides.