Full page caching for headless Shopware

city with lights of fast cars

Caching Nuxt server rendered pages with cache tag support

Recently we went over the challenge of how to implement HTTP full page caching for a headless frontend build as a Nuxt application. The motivation for this was that apart from the standard Shopware TWIG storefront, in a headless frontend you don't get a full page caching out of the box. But to be able to get good server response times (assuming serverside rendering is used for the frontend), a FPC is essential. This simply boils down to the fact, that on every server side render, the response time is highly dependent on the response time of the APIs used (in this case Shopware) to fetch the content. But for many pages the contents do not change that often, so it's not necessary to fetch the information and even do the rendering for every request.

As always with caching the hard part is not the implementation of the caching itself, but rather the cache invalidation process. This is especially true for headless frontends which rely on another system for the actual displayed content. Because of the nature of a headless architecture, the backend (here: Shopware) doesn't know about the frontend. Therefore it obviously can't do the caching and its invalidation by default.

Flow-Chart of the caching process

So we took inspiration from different projects, namely VueStorefront, Drupal and Shopware itself, to build a flexible solution to handle cache invalidation for a headless Shopware frontend. We're proud to announce the availability of two reusable parts, a nuxt module and a Shopware bundle.

Nuxt module

The nuxt module does essentially three things:

  1. it provides a nuxt plugin which adds a simple store to collect the cache-tags.
  2. it provides a composable for easily adding different cache-tags
  3. it hooks into the nuxt lifecycle to add the HTTP X-Cache-Tags header with all relevant cache-tags before sending the response

You can learn more about it on GitHub https://github.com/mothership-gmbh/nuxt-shopware-caching

Shopware bundle

The bundle essentially registers another cache-item pool, so that everytime a internal Shopware cache-tag gets invalidated, the bundle knows about it and can therefore do something with this information.
For every cache-tag invalidation it essentially triggers a HTTP BAN-Request to the configured hosts, where Varnish runs and flushes the corresponding pages correctly.
You can learn more about the Shopware bundle on GitHub https://github.com/mothership-gmbh/headless-shopware-varnish-cache

Difference to the existing Varnish-Adapter

The "official" solution for the standard storefront uses an additional DB (Redis) to provide a mapping between Cache-Tag and URL. During the cache invalidation, first the URLs to invalidate are fetched from the DB using the cache-tag, then the BAN-Request is send using the URL.

We simplified this solution to use the cache-tags directly. All cache-tags used in a page get aggregated into a HTTP-Header X-Cache-Tags, so Varnish has this information available. The invalidation request then contains the header, too, so Varnish can then invalidate all pages with the specified cache-tag in the pages header directly.

Advantages

  • simplified setup with only one more dependency (Varnish)
  • manual invalidation is simpler, because you can build the cache-tag simply by yourself. Every page containing this cache-tag will be invalidated automatically, no need to know all URLs

Disadvantages

  • potentially big X-Cache-Tag header in Varnish (e.g. for product listing pages)
  • more work for varnish as for the invalidation regex-matching has to be done

Varnish configuration

We provide a stripped down example Varnish configuration here.

Disclaimer: this is not a production ready configuration file and stripped down to show the essential parts for the caching solution!

Future

  • see how the approach using the tags as a HTTP header works out. If it gets too big, maybe rely on another system like Redis to provide a mapping from cache-tags to url, so that the urls that have to be invalidated can be queried beforehand. The invalidation would then only use the url itself as the identifier.
  • use Varnish xkey instead of a header, as this would be much more performant.
  • migrate the Shopware bundle to a Shopware app, so that this could also be used for Shopware backends hosted in the cloud. The current limitation here is, that up to this point there is no way for Shopware apps to subscribe to the internal invalidation events, which is necessary for this usecase.
  • add additional cache related HTTP headers to the Nuxt-module. For example Cache-Control to be able to provide information dynamically of how and even if the current page should be cached.
  • ESI-like functionality to be able to cache parts of the HTML. This would be especially useful for sections like the pages header or footer. In the current implementation a single change in the footer invalidates essentially all pages.
    From my current perspective and knowledge this is quite hard as it requires to be able to render only parts of the whole component tree. It's especially hard if you take global application state into account. During my short research I came across react-esi which provides ESI-functionality for React application, so maybe it's worth a look and adaptation for Vue.