CMSPerformanceCaching

How to improve performance by caching content using Strapi & SvelteKit

Ainsley Clark Photo

Ainsley Clark

Dec 3, 2023 - 4 min read.

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:

  1. When a request is made to Strapi, the cache is looked up by the request key.
  2. If the data is persisted in the cache layer, it will be returned immediately.
  3. If it isn’t persisted, it will create the request and store it in memory.
Cache ADR Diagram

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.

Cache Webhook Diagram

1) Invalidation endpoint

To begin with, an endpoint needs to be created within SvelteKit to bust the cache.

This endpoint:

  1. Checks to see if the API key is valid.
  2. 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 SettingsWebhooks and create a new webhook with the URL of the SvelteKit application along with an API key.

Strapi Webhook

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

TTFB Before

After

TTFB 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.

About the author

Ainsley Clark Photo
Ainsley Clark

CEO & Director, Designer & Software Engineer

Ainsley Clark is a senior full-stack software engineer and web developer. As the company's founder, he is in charge of each key stage of our website and software builds and is our clients' reliable point of contact from start to finish of every project. During his extensive experience in the industry, Ainsley has helped numerous clients enhance their brand identity and increase their ROI with his custom designed websites, as well as constructed and deployed a vast array of highly scalable, tested and maintainable software applications, including SEO tools and APIs.

Follow me on: