Hydrogen is a framework designed specifically for developing customized, modern, performant e-commerce sites powered by the Shopify platform. But with Netlify’s advanced caching primitives, we can take performance even further.
Countless studies have found that improvements in page load time directly lead to improvements in conversion rate. If you’re running an e-commerce storefront, you have a vested interest in squeezing the best possible performance out of your pages.
In this guide, we’ll show you a standards-based approach where static and infrequently changing content is cached with HTTP response headers and uncacheable content (say, a cart and session) is then loaded dynamically from the browser, combining the best of both worlds.
Read on to find out how to get content in front of your customers faster.
#A quick primer on Hydrogen rendering
Hydrogen is based on the Remix framework. Remix does not support pre-rendering pages at build time. When initially loading a page from a browser, Remix uses Server-Side Rendering (SSR) to render it on the fly from the server (an edge function running on a Netlify CDN edge node close to the user).
Hydrogen uses Remix’s SSR streaming to stream this response to the browser, allowing content to start being shown as soon as possible. Any components on the page using React Suspense are then streamed in as they become ready, after the closing </html>
tag in the same response (yes, that’s how it works under the hood!).
This pattern has many benefits, but one downside is that despite having separated the fast initial shell of the page from the more dynamic content loaded in later, the initial shell isn’t cacheable on its own without having the dynamic content tag along!
What about other frameworks?
The same would be true when using React Suspense directly without a meta-framework like Remix.
The same is true with Next.js’s experimental Partial Pre-rendering (PPR) feature, which uses React Suspense.
On the other hand, Astro’s experimental Server Islands are cacheable out of the box. They use an approach much like the one in this guide.
#Getting started
This guide assumes you already have a Hydrogen site deployed to Netlify. If that isn’t the case, you can follow this guide first then come back here to set up caching.
Just want to deploy a new site with all the caching bells and whistles? Click the button below. All the steps below will have been completed for you.
If you prefer to just follow along without deploying your own site, check out our demo store. It follows all the patterns described below. Just keep in mind that you won’t be able to try on-demand revalidation for yourself.
#Caching pages with Hydrogen
OK, so how do we cache pages on a Hydrogen site?
- Avoid React Suspense for content you don’t want to cache
- Fetch that data from the browser instead with Remix’s
useFetcher
hook - Set cache response headers
#1. Avoid React Suspense for content you don’t want to cache
First, stop using Remix defer()
for any data you
don’t want to cache. For example, you shouldn’t cache personalized content like a cart or a
logged-in user’s session.
#2. Fetch the above data from the browser with Remix’s useFetcher()
For each unique piece of data you updated in step 1 above, make sure your app provides an “endpoint”
to fetch it. With Remix, you can fetch any route that has a
loader()
, so
you may not need to make any changes in most cases. For example, this /cart
route works as is:
Now we can fetch this data from the browser. Let’s use React Context to avoid duplicating code, ensure we handle errors, ensure it only runs in the browser, and ensure type safety:
Provide the cart context in your app root:
Using AnalyticsProvider
?
If you’re using AnalyticsProvider
, check out this
approach for composing these two
context providers.
Finally, we can use this context instead of React Suspense everywhere this data is used:
Add a fallback
Make sure to handle the undefined
case that was previously handled by the <Suspense>
’s fallback
prop!
#3. Set cache response headers
At this point, you can set any standard HTTP caching headers and compose them to meet your site’s needs.
To cache a page, export a Remix headers()
function from the route. For example:
In this example, we instruct the Netlify CDN to cache the homepage for up to one hour and instruct browsers to use cached content indefinitely as long as they check with Netlify that it isn’t stale.
The Netlify-Vary
header is
needed because Remix uses each of your app’s routes for two purposes: rendering pages and fetching
that page’s data. Remix uses a ?_data=...
query string parameter to identify which to load (think
of it like an Accept
header). Luckily, Netlify-Vary
solves this perfectly by caching them
separately. Make sure you always set this header, on page responses and on loader responses.
If you load this page multiple times with your browser’s developer tools open, you should see something like this:
The first request to the homepage (/
) invokes the SSR edge function and results in a response time
over 600 ms. Since we’ve instructed this page to be cached for an hour on the CDN, subsequent
requests are an order of magnitude faster, clocking in around 30 ms.
#What about CSR? Let’s cache data too
Now, your customers don’t hit reload over and over. You may have noticed that if you make the changes above and click around your site like an actual person, response times aren’t quite as fast as the 30 ms on reload and if you inspect the requests you may notice no caching is leveraged.
This is because after the initial page load and app hydration, Remix uses client-side navigation, determines what data to fetch for the next page, fetches it, then renders the new page on the client. This is commonly called Client-Side Rendering (CSR).
If we want to leverage caching for CSR, just set cache headers on loader responses. For example:
Try it again and you’ll see that client-side navigation to the /policies
route is now consistently
fast because it leverages data cached on the Netlify CDN.
#Stale-While-Revalidate (SWR) caching with Hydrogen
Stale-While-Revalidate or SWR is a pattern that lets you render a page on the fly via SSR on the first request to a route (for that CDN node), and subsequently always serve responses directly from the CDN edge cache and control when that page is regenerated in the background. Read more about it in our docs.
To use it, just add the directive on any page or loader:
In this example, we:
- Instruct the Netlify CDN to serve cached responses for the homepage (
/
) even if they are stale (older than one hour), and initiate a revalidation in the background if it is stale - Instruct the Netlify CDN not to serve stale responses if it’s been more than two minutes since we initiated the background revalidation (this is a failsafe)
- Instruct browsers to use cached content indefinitely as long as they check with Netlify that it isn’t stale.
If you load this page multiple times, here’s what happens:
- The first request will invoke your Hydrogen SSR edge function.
- Subsequent requests (that hit the same CDN node) will respond with cached content immediately.
- Requests an hour or more later will also respond with stale cached content immediately, but within a second or two subsequent requests will respond with updated (but still cached!) content immediately.
#Vary
and Netlify-Vary
with Hydrogen
Netlify supports Vary
as well the more powerful
Netlify-Vary
header. Let’s take a
look at a couple common use cases.
#Search page
On a search page, you may want something like this:
This will cache search results for 5 minutes, separately by search query and options, and separately for page responses and data responses.
#Product detail page
Out of the box, Hydrogen distinguishes product
variants by breaking
out a given variant’s options in the URL query string, e.g.
/products/sweater?Size=medium&Color=blue
. To cache these separately and efficiently, we can vary
on these query string params:
This works, but it is isn’t very robust, since each product can have its own set of options, and these can change at any time. How do we solve this once and never worry about it again? When rendering, we already access to the product data that we’ve loaded from the Shopify GraphQL API, so we can read the options from there and derive the vary header from those:
#On-demand cache invalidation on updates to your Shopify store
We can take this even further. For content that is dynamic but isn’t constantly changing, we can configure an aggressive caching policy and configure a Shopify webhook handler on our Netlify site that performs on-demand tag-based invalidation of stale content.
When returning product data, add a cache tag uniquely identifying that product:
(The above assumes you’re using the pro-tip below to share your loader’s headers with your page’s.)
What about pages with multiple products?
For a page listing multiple products, you can choose from a few options. You could follow this same approach and
include a tag for each product id on the page. You could set a general cache tag on these pages (e.g. products
) and
always invalidate this tag on any product update. You could set a relatively low TTL (e.g. max-age=300
, 5 mins) on
these pages and not bother with on-demand revalidation at all. The choice is yours.
Then, add a Shopify product update webhook event handler. Let’s use a Netlify serverless function:
There’s a bit of boilerplate required here to implement this securely. You can copy this as is into a new file:
Then, create the Shopify webhook:
- In your Shopify store, navigate to
Settings > Notifications > Webhooks
- Click “Create webhook”
- Choose the “Product update” event (or whichever applies to your case)
- Enter
https://<your-site-name>.netlify.app/.netlify/functions/shopify-webhook
for the URL, where<your-site-name>
is your site name (which is also its subdomain) andshopify-webhook
is your webhook handler function name - Choose an API version. This doesn’t need to match the API version used by your site. We recommend the latest stable version.
- Click “Save”
On the same page, below “Your webhooks will be signed with” you’ll find your Shopify webhook secret. Copy this to your clipboard and run:
Netlify CLI
Commands starting with netlify <...>
use the powerful Netlify CLI. Install it by running:
You’re all set! If you deploy these changes, cached contents for /products/abc
will be immediately
purged across the Netlify global CDN.
Note that caching is scoped by deploy context and domain, so if you’re clicking around a Deploy Preview (e.g.
https://deploy-preview-1234--example.netlify.app/products/abc
) but you only have a webhook configured for
https://example.netlify.app
, that Deploy Preview will be unaffected. Luckily, you can create as many webhooks as you
want, with different URLs.
If you’re curious, you can stream the webhook handler function logs locally:
#Pro-tip: reuse your Hydrogen cache config across your app
A more realistic example of the above would show both the loader()
response and the page response,
both specifying similar or identical cache headers. Luckily, Remix provides mechanisms for
referencing loader headers in page headers and even
parent route headers. A more convenient setup would look something like this:
Now the page will inherit its loader’s headers, unless overridden. It will even inherit any headers from its ancestor routes. You could use this to define default page cache headers across your whole site and only override them when necessary.
Unfortunately, there is no such mechanism for loader headers. The only tool in the Remix toolbelt to
help with duplication and consistency here is to intercept all data requests and attach headers. You
can do this by updating your app/entry.server.jsx
:
For full composability, export default caching headers from a helper module and extend them where needed.
#Wrapping up
In this guide, we demonstrated how to leverage Netlify’s advanced caching primitives to get Hydrogen e-commerce storefront pages in front of customers an order of magnitude faster.
We accomplished this by using a hybrid approach where pages and data are cached with HTTP response headers—as with SSG—and uncacheable components and data are loaded from the browser—as with CSR. The end result is similar to Partial Prendering, but the implementation builds on the web platform.
#Live demos
Compare two versions of the same demo storefront: