Combining Strapi with SvelteKit or other JS frameworks can make for a formidable tech stack suitable for various business platforms. However, adopting a headless approach might impact page speed if requests are not server-side propagated, or if the front-end stack and Strapi are deployed with different providers or in different regions. Today, we explore caching in SvelteKit to boost your Time to First Byte (TTFB) and UX.
Rest Cache
One notable plugin in the Strapi Marketplace is Rest Cache. This allows you to cache HTTP requests without extra calls to the database. It has multiple cache providers, including:
Memory
Redis
Couchebase
Custom
While Rest Cache effectively reduces database call times by caching content on the CMS side, it does not address the inherent latency in direct requests from the JavaScript framework to the server, which can still impact overall page load times.
SvelteKit caching
Although the Rest Cache method works wonders to improve response times from the Strapi database, there may be times that you need extra performance when making these requests. A practical solution for enhancing performance between these frameworks is the implementation of node-cache, a straightforward caching module with functionalities akin to memcached and redis.
Here’s a step-by-step breakdown:
- When a request is made to Strapi, the cache is looked up by the request key.
- If the data is persisted in the cache layer, it will be returned immediately.
- If it isn’t persisted, it will create the request and store it in memory.
Creating the caching mechanism
It’s useful to have a utility class or type that is responsible for requesting all data from Strapi. This will allow you to set and clear the cache on all POST requests. Below is a small tutorial on how to implement the architecture as defined in the ADR above.
1) Install Node Cache
npm install node-cache
2) Creating a cache class
A singleton cache instance will help as a wrapper for node-cache
, which can be placed in a utility or service package.
lib/server/cache.ts
1import NodeCache from 'node-cache';
2
3class SvelteCache {
4 private cache: NodeCache;
5
6 /**
7 * Constructor for the Cache class.
8 */
9 constructor() {
10 this.cache = new NodeCache();
11 }
12
13 /**
14 * Retrieves a value from the cache.
15 * @param {string} key The key to retrieve from the cache.
16 * @returns {T | undefined} The cached value or undefined if not found.
17 */
18 get<T>(key: string): T | undefined {
19 return this.cache.get<T>(key);
20 }
21
22 /**
23 * Sets a value in the cache.
24 * @param {string} key The key to store the value under.
25 * @param {T} value The value to store in the cache.
26 * @param {number} ttl The time to live in seconds. Optional.
27 * @returns {boolean} True if the value was successfully stored.
28 */
29 set<T>(key: string, value: T, ttl?: number): boolean {
30 return this.cache.set<T>(key, value, ttl);
31 }
32
33 /**
34 * Invalidates the entire cache.
35 */
36 invalidateAll(): void {
37 this.cache.flushAll();
38 }
39}
40
41const Cache = new SvelteCache();
42export default Cache;
3) Add the mechanism
Cache retrieval
First off, we need to check if the item resides in the cache. If it does, we can serve the data from memory. The
environment should also be checked for development
so the cache can be disabled when editing.
1// Try and retrieve the call from the cache.
2if (method == 'GET' && !dev) {
3 const cachedData = Cache.get<T>(url);
4 if (cachedData) {
5 return Promise.resolve(cachedData);
6 }
7}
Cache setting
After the request, we need to persist the data to the cache using the Strapi url as a key.
1// Set the entity in the cache layer.
2Cache.set(url, json);
All together
Below is an example of the cache layer tied together. The find
method is used to retrieve content type entities, but
we can easily extend this to other methods that utilise GET
requests, such as findOne
or findBySlug
1import { stringify as QueryStringify } from 'qs';
2import type {
3 StrapiResponse,
4 StrapiBaseRequestParams,
5 StrapiRequestParams,
6 StrapiError,
7} from 'strapi-sdk-js';
8import { dev } from '$app/environment';
9import { PUBLIC_STRAPI_URL } from '$env/static/public';
10import Cache from '$lib/server/cache';
11
12class Strapi {
13
14 /**
15 * Basic fetch request for Strapi
16 *
17 * @param method {string} - HTTP method
18 * @param url {string} - API path.
19 * @param data {Record<any,any>} - BodyInit data.
20 * @param params {StrapiBaseRequestParams} - Base Strapi parameters (fields & populate)
21 * @returns Promise<T>
22 */
23 async request<T>(
24 method: string,
25 url: string,
26 data?: Record<any, any>,
27 params?: Params,
28 ): Promise<T> {
29 url = `https://strapi.com/api${url}?${params ? QueryStringify(params) : ''}`;
30
31 // Try and retrieve the call from the cache.
32 if (method == 'GET' && !dev) {
33 const cachedData = Cache.get<T>(url);
34 if (cachedData) {
35 return Promise.resolve(cachedData);
36 }
37 }
38
39 return new Promise((resolve, reject) => {
40 fetch(url, {
41 method: method,
42 body: data ? JSON.stringify(data) : null,
43 })
44 .then((res) => res.json())
45 .then((json) => {
46 if (json.error) {
47 throw json;
48 }
49 // Set the entity in the cache layer.
50 Cache.set(url, json);
51 resolve(json);
52 })
53 .catch((err) => reject(err));
54 });
55 }
56
57 /**
58 * Get a list of {content-type} entries
59 *
60 * @param contentType {string} Content type's name pluralized
61 * @param params {StrapiRequestParams} - Fields selection & Relations population
62 * @returns Promise<StrapiResponse<T>>
63 */
64 find<T>(contentType: string, params?: StrapiRequestParams): Promise<StrapiResponse<T>> {
65 return this.request('GET', `/${contentType}`, null, params as Params);
66 }
67}
Cache Invalidation
What about the infamous cache invalidation? Strapi provides a great web-hook feature that can be triggered every time a user updates an entry, which can be used to bust the SvelteKit cache. This can be achieved using the steps below.
1) Invalidation endpoint
To begin with, an endpoint needs to be created within SvelteKit to bust the cache.
This endpoint:
- Checks to see if the API key is valid.
- Calls
cache.invalidateAll()
to remove all items from the cache.
api/cache/+server.ts
1import { json } from '@sveltejs/kit';
2import Cache from '$lib/server/cache';
3
4/**
5 * POST /cache
6 * Endpoint to flush the cache, typically called from a Strapi webhook.
7 */
8export async function POST({ request }) {
9 if (request.headers.get('X-ADev-API-Key') != "My Secret API Key") {
10 return json(
11 {
12 success: false,
13 message: 'Invalid credentials',
14 },
15 {
16 status: 401,
17 },
18 );
19 }
20
21 // Flush the entire cache.
22 Cache.invalidateAll();
23
24 console.log(
25 'Cache successfully flushed at: ' +
26 new Date().toLocaleTimeString('en-GB', { hour12: false }),
27 );
28
29 return json({
30 success: true,
31 message: 'Cache successfully flushed',
32 });
33}
2) Setting up the webhook
Now we can set up a webhook to invalidate the cache every time an entry is updated within Strapi. Navigate to Settings
➝ Webhooks
and create a new webhook with the URL of the SvelteKit application along with an API key.
Results
We noticed a significant drop in TTFB for implementing the above method. Average request times were reduced by about 600ms, which is a huge boost in performance and page speed.
Before
After
Going further
In advancing our caching strategy, we can refine our approach to cache invalidation by targeting specific endpoints. For
instance, if a user accesses the /blog
endpoint, we have the capability to invalidate the cache specifically
for /blog
, rather than clearing the entire cache.
Wrapping up
In summary, the integration of caching mechanisms in a Strapi and SvelteKit setup significantly enhances performance, notably reducing TTFB and improving UX. By implementing strategies such as using Rest Cache for CMS side optimisation and node-cache for direct JavaScript framework requests, we can effectively mitigate latency issues. This approach not only streamlines content delivery but also exemplifies the synergy between strategic caching and modern web technologies, ultimately leading to faster and more responsive web applications.