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:
Nitro | Nuxt underlying open source framework to build web servers using unjs/h3 |
Nitro - Storage Layer | Nitro has built-in integration with unjs/unstorage to provide a runtime agnostic persistent layer. |
Unstorage | Unified 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-cache | storefront-session |
---|---|
Provides storage access to the global cache storage | Provides 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. rpcCalls | Used 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. |
auth | can 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
cacheKey | allows 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>')
})
})