docs
  1. SCAYLE Resource Center
  2. Developer Guide
  3. Basic Setup
  4. Caching

Caching

Overview

This guide explains the fundamentals of caching and its implementation within the Storefront Boilerplate. Storefront Boilerplate uses hybrid rendering. This approach combines server side rendering (SSR), Single Page Application (SPA), and Static Site Generation (SSG). Depending on the page content, the most appropriate mode is chosen automatically.

Storefront Boilerplate relies heavily on server-side caching. This aims to reduce the load on the server and minimize the time and resources required to generate dynamic content for each user request. By caching the results of expensive API calls, or entire rendered pages, server-side caching optimizes response times, decreases server processing overhead, and ultimately delivers a smoother and more efficient user experience.

Before you start

Familiarize yourself with:

NitroNuxt underlying open source framework to build web servers using unjs/h3
Nitro - Storage LayerNitro has built-in integration with unjs/unstorage to provide a runtime agnostic persistent layer.
UnstorageUnified key-value storage API, supports conventional features like multi-driver mounting, watching, and working with metadata

Key concepts

Hybrid Rendering

Hybrid rendering allows different caching rules per route using Route Rules and decides how the server should respond to a new request on a given URL. Nuxt 3 includes route rules and hybrid rendering support.

Using route rules you can define rules for a group of Nuxt routes, change rendering mode, or assign a cache strategy based on route!

Hybrid rendering in detail

In Hybrid Rendering, the framework intelligently chooses between SSR and SPA based on the nature of the route. For pages that benefit from pre-rendering on the server for SEO and initial load performance, SSR is used. On the other hand, for pages where a more dynamic and interactive experience is suitable, the client-side rendering (CSR) approach of SPA is employed. This hybrid approach is designed to provide the advantages of both SSR and SPA, optimizing the balance between server-side rendering for initial page loads and client-side rendering for subsequent interactions. It helps developers achieve better performance and SEO while maintaining a dynamic user experience.

The Nuxt server of the Storefront Boilerplate will automatically register corresponding middleware and wrap routes with cache handlers using Storefront Core Cache setup based on the Nitro caching layer.

Check the latest Nuxt documentation for any changes or additions.

Route Rules

Nuxt, specifically its underlying web engine nitro allows the addition of logic at the top level of the nuxt.config.ts configuration. This is useful for redirecting, proxying, caching and adding headers to all or specific routes. By default, Nuxt uses In-Memory storage via the Nitro - Caching API for caching data internal. This applies also to data handled via routeRules if not configured differently, e.g. server-side rendered page responses.

As caching page responses purely in-memory can drastically increase the memory footprint of the runtime the Storefront application is running, the Storefront Boilerplate provides a preconfigured cache storage to be used.

Define routeRules

Nuxt also provides the option to define routeRules, called inlineRouteRules, at the page level using defineRouteRules. This feature is currently marked as experimental and must be explicitly enabled. For more details check the Nuxt - defineRouteRules documentation.

As the Storefront Boilerplate currently does not rely on features marked experimental, no direct support is provided for this feature or other experimental Nuxt features. Should you wish to use this feature, this can be implemented at your own risk!

For more in-depth details on how routeRules work check the Nitro - Route Rules documentation.

Cache storage

Nuxt, and its underlying web engine nitro, provide a built-in storage layer that can abstract filesystems, databases, or any other data source. This allows to use of different data sources for caching like In-Memory, Redis, LRU, VercelKV, and many more.

Nuxt uses Unstorage as its primary way of interacting with data sources.

Build on top of unstorage and the Nitro - Storage Layer, nitro provides a powerful caching system.

Pre-configured storage mountpoints

Storefront Core provides two global default storages storefront-cache and storefront-session.

storefront-cachestorefront-session
Provides storage access to the global cache storageProvides storage access to the global session storage
Can be used for page caching with routeRules in nuxt.config.ts

Shop-specific storage mountpoints

Additionally, we create dedicated storage mountpoints for every shop instance using the former storage mount names plus the respective shopId. This results in every shop having two dedicated storage mountpoints storefront-cache:{shopId} and storefront-session:{shopId}.

By default, they will reuse the global storefront.storage.cache or storefront.storage.session configuration or the respective shop-specific configuration options.

storefront-cache:{shopId}storefront-session:{shopId}
Used for cached method to cache responses of e.g. rpcCallsUsed for dedicated handling of user sessions on server-side
Used for saving redirects from SCAYLE SCAYLE Panel (if enabled)
Example shop-specific mountpoints

A Storefront application with the two shopIDs of 10001 and 10002 will results in the following additional shop-specific mountpoints:

  • storefront-cache:10001
  • storefront-session:10001
  • storefront-cache:10002
  • storefront-session:10002

Providing shop-specific storage mountpoints allows for different storage configurations per shop and can e.g. be used for storage sharding or separating data residency.

Storefront storage cache configuration

The storage configurations for the respective pre-configured storage mountpoints are based on storefront.storage.cache and storefront.storage.session key. This unifies their usage across a Storefront-based application and allows for deeper integration between Storefront Core and the Storefront Boilerplate.

storefront-cache is not related to the default cache mountpoint used by Nuxt and Nitro. The cache mountpoint will continue to be available as outlined in the Nitro - Cache API documentation.

Both, the storefront.storage.cache and storefront.storage.session configuration are compatible with the key value of the Nitro - StorageMounts Interface. This should ensure that the storefront.storage configuration is mostly compatible with the global nitro.storage configuration options of a nitro Nuxt application.

Storefront Core uses an abstract-type StorageConfig interface has the following structure, where cache and session are optional. Should no dedicated StorageEntity be configured to cache or session, Storefront Core will use the In-Memory driver as default for both storage mountpoints.

It is not recommended to use the In-Memory driver for production environments, as it can lead to high memory consumption and potentially harmful memory leaks! Additionally, cached data, e.g. user session, can be lost if an instance of the server runtime (e.g. node) crashes or cached data might not be shared between multiple server runtime instances.

interface StorageConfig {
  cache?: StorageEntity
  session?: StorageEntity
}

The StorageEntity interface looks similar to the following, but includes some additional "type magic" to use the proper driver options based on the set driver key:

interface {
    driver: BuiltinDriverName | CustomDriverName;
    [option: string]: any;
  }
Example nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    storefront: {
      storage: {
        cache: {
          driver: 'redis',
          compression: 'gzip,
          host: 'localhost',
          port: 6379,
        },
        session: {
          driver: 'redis',
          compression: 'none',
          host: 'localhost',
          port: 6379,
          db: 1,
        },
      },
      cache: {
        enabled: true,
        auth: {
          username: 'max',
          password: 'mustermann',
        },
      },
    },
  },
})
Example config
let config = {
    storage: {
        cache: {
            driver: 'redis',
            compression: 'gzip',
            host: 'localhost',
            port: 6379,
        },
        session: {
            driver: 'redis'.
            compression: 'none',
            host: 'localhost',
            port: 6379,
            db: 1,
        },
    }
}

The configuration key driver for eitherstorage.cache or storage.session allows specifying one of the supported unstorage Drivers to be used. All following option keys are related to the chosen driver and change depending on it. Please consult the documentation of the selected unstorage Driver.

The optional compression key accepts a value of deflate, gzip, brotli or none to specify the used encoding algorithm to compress all data of the specific storage. After setting the compression option, it is recommended to empty the corresponding storage instance/database, e.g. Redis.

The storefront configuration option redis is deprecated and should not be used anymore. It has been replaced by this dedicated storage configuration.

For a list of available unstorage drivers, check the Unstorage Documentation.

Compression support

The storage configuration supports compression of all handled data and can be configured via the optional compression key.

The optional compression key accepts a value of deflate, gzip, brotli or none to specify the used encoding algorithm to compress all data of the specific storage.

After setting the compression option, it is recommended to empty the corresponding storage instance/database, e.g. Redis.

Custom Unstorage Compression Driver

The following outlines the underlying technical foundation for the included compression support of the Storefront Core Nuxt module. It is not required to manually add the NPM package @scayle/unstorage-compression-driver to your project.

Storefront Core implements a custom unstorage driver as a dedicated and publicly available NPM package @scayle/unstorage-compression-driver. This driver is able to take a passthroughDriver as its primary storage handler and passed the respective functions like getItem() or setItem() to it. In between the custom compression driver is trying to serialize or deserialize the values, as well as trying to compress/decompress based on a passed encoding option. Before passing the serialized and compressed value to the actual passthroughDriver, the handled value is base64 encoded during the compression process.

Supported compression encoding

For the first version we're relying on and supporting deflate, gzip and brotli based on node:zlib compression encoding.

Configuration of compression encoding

It is currently not possible to pass dedicated options to the selected compression encoding function of node:zlib. This means that the compression is currently using the default options of the Node.js runtime.

Integration into storefront-nuxt

The value compression is directly integrated with the storefront.storage configuration. To keep the usage of the respective storefront.storage.cache and storefront.storage.session configuration as simple as possible, a new option compression has been introduced on the StorageEntity interface. Instead of requiring to pass the custom driver name compression to the driver key and setting passthroughDriver to the desired unstorage driver like redis, Storefront Core abstracts and handles the selection of which driver is loaded and passed internally in a Nitro server plugin.

A consuming Storefront application using the new storefront.storage configuration just needs to add the compression key to either or both storefront.storage.cache and storefront.storage.session, as well as passing one of the supported compression encodings as a string (deflate | gzip | brotli) to enable value compression for the respective unstorage mountpoint.

Additionally, it is possible to either remove the compression key or set its value to none to disable cache compression.

Internal storage mountpoint handling

Internally the runtime configuration key of storefront.storage is used to register the storage mountpoints cache and session during the server startup. For this Storefront Core is adding an async Nitro - Plugin, called nitroRuntimeStorageConfig to hook into the startup of the server.

Implementation

Page caching in Storefront Boilerplate

The distributed default configuration of the Storefront Boilerplate comes with page caching enabled and relies on the global storefront-cache storage mountpoint available via Storefront Core - Caching and configured via Storefront Core - Storage via Module Configuration.

Page Caching via routeRules within the Storefront Boilerplate is enabled for all pages (see configuration key *). As not all pages should be cached, the Storefront Boilerplate explicitly disables caching for certain API or user-specific routes via cache: false.

For an in-depth overview of the available routeRule configuration options per route, check the Nuxt - Route Rule documentation.

Configure cache

cache.enabled controls enabling or disabling the cache. By default, it is enabled. When disabled, the cache utility will always execute the handler function and will not check the cache or save the results to the cache.
authcan be used to protect the cache control endpoints with Basic Authentication credentials.
let config = {
    cache: {
        auth: {
            username: '',
            password: '',
        },
        enabled: true
    }
}

The session configuration option session.provider is deprecated and should not be used anymore. It has been replaced by a dedicated storage.session configuration. See storage for more details.

Example
defineNuxtConfig({
    ...allOtherNuxtProjectConfiguration,
    routeRules: {
        // Page generated on-demand, revalidates in background
        '*': {
            cache: {
                swr: true, // Enable stale-while-revalidate
                maxAge: 60 * 60, // Default: 1h
                staleMaxAge: 60 * 60 * 24, // Default: 24h / 1d
                group: 'ssr', // Cache group name
                name: 'page', // Set prefix name
                // Use storefront storage mount
                // Depending on your configuration this might be `redis` or another database driver
                // https://scayle.dev/en/dev/storefront-core/module-configuration#storage
                base: 'storefront-cache',
            },
        },
        // Don't cache API routes.
        '**/api/**': { cache: false },
        '**/api/rpc/**': { cache: false },
        // Don't cache pages with user-specific information
        '**/wishlist': { cache: false },
        '**/basket': { cache: false },
        '**/checkout': { cache: false },
        '**/signin': { cache: false },
        '**/account/**': { cache: false },
        '**/orders/**': { cache: false },
    },
}

Use cache in RPC methods

The RpcContext object includes a cached function. It takes as its first argument a function to be executed, and cache options as its second argument. It returns a wrapped version of the passed-in function that caches the result of the function and will re-use the result on subsequent calls.

Available cache options

cacheKeyallows specifying an explicit cache key. If there is an entry in the cache matching cacheKey, the function will not be called and the cached result will be returned instead. If cacheKey is not passed as an option, the cache key will be automatically generated
cacheKeyPrefix is used when generating the cache key. The generated cache key will be cacheKeyPrefix concatenated with a hash of the arguments passed to the cached function.
ttl defines (in seconds) how long a cache entry should live.
timeout allows timing out cache requests. If the cache does not return a result within the timeout period, it will execute the function instead. If timeout is not specified, a timeout of 500ms will be used. The timeout only applies to fetching from the cache
type CacheOptions = {
  cacheKey: string
  cacheKeyPrefix: string
  ttl: number
  timeout: number
}
Example RpcComtext
async function getProduct(context: RpcContext, id: number) {
  return await context.cached(context.sapiClient.products.getById, {
    cacheKeyPrefix: `getById-product-${options.id}`,
  })(options.id)
}

Disable cache

The Storefront Core internal cache, using cached can be enabled/disabled at build-time by setting the module configuration cache.enabled or at runtime via NUXT_STOREFRONT_CACHE_ENABLED.

Clear cache

The cache can be cleared by sending requests to specific endpoints.

purge/All

The /api/purge/all endpoint will clear the entire cache

curl -X POST 'http://localhost:3000/api/purge/all'

purge/tags

The /api/purge/tags endpoint will clear all cache keys for the specified tags

curl -X POST -H 'Content-type: application/json' '<http://localhost:3000/api/purge/tags>' --data '["asdf"]'

Purge Endpoint Authentication

So that random people don't invoke these endpoints, they can be protected by basic authentication. To enable basic authentication, cache.auth should be set in the module options with a username and password property. Requests will then need to include the Authorization header.

curl -X POST -H 'Authorization: Basic bWF4Om11c3Rlcm1hbm4=' 'http://localhost:3000/api/purge/all'

Test caching with Vite

Compatibility with Vite tests

Depending on the Vite test setup it might be necessary to override the default storefront.storage configuration, and to use driver: 'memory' for testing purposes.

Example pseudo test case
describe('Pseudo storefront.storage override test case', async () => {
  await setup({
    nuxtConfig: {
      storefront: {
        storage: {
          cache: {
            driver: 'memory',
          },
          session: {
            driver: 'memory',
          },
        },
      },
    },
  })

  it('renders the index page', async () => {
    // Get response to a server-rendered page with `$fetch`.
    const html = await $fetch('/')

    expect(html).toContain('<div>something</div>')
  })
})

References

Nitro