Skip navigation

Improve performance with caching

14 min read
Download PDF

Make your apps more responsive, resilient, and generally perform better with caching. Caching helps eliminate the wait time that you and the users of your app would otherwise face when your app gets content from an API or experiences connection issues.

Table of contents

    Key points

    • A cache is an extra layer with saved responses from Kontent's APIs.
    • Implement client-side caching with time-based invalidation for basic apps.
    • Use webhooks if you need to keep your app's content always up to date. You need to add a server-side proxy to receive webhook notifications.
    • Mobile apps caching is similar to the web apps caching approaches.

    Cache in a nutshell

    A cache is an additional layer of storage used to quickly retrieve data in your application. With Kontent, cache sits between your app and a Kontent's API. The cache contains entries with responses from the API. The entries are referenced by a key. 

    When your app fetches content, it first checks the cache for a response with the specified key. If the response is available in the cache, there is no need to send a request to an API. If the response isn't cached, the app makes an API request and stores the result in the cache. The cache needs to be invalidated when you update your content.

    Because certain parts of your content may depend on other parts, cache entries must specify their dependencies. Your app puts an API response in the cache together with special dummy cache entries that are empty and specify the dependencies of that response. When your app invalidates cache, it also invalidates the relevant dummy entries so that responses that depend on these dummy entries are invalidated as well.

    A diagram showing cache entries and their dependencies.

    Cached responses and their dependencies on Kontent objects.

    Decide: client-side or proxy with webhooks?

    For most applications, we recommend that you build your app's cache layer without any webhook logic for two reasons:

    1. It's much easier to implement.
    2. It performs better because your app doesn't need to invalidate cache entries every time there's been a change in the project.

    To implement the no-webhooks time-based approach, set cache expiration to a specific amount of time. The expiration time should correspond to your users' expectations and their tolerance to outdated content.

    If you need to display content that's always up to date, use webhooks with server-side proxy to cache content and receive the webhook notifications. Whenever your proxy receives a notification about changes to published content, it needs to invalidate specific items in its cache.

    Basic caching for client-side apps

    The following scenario is built on the example of a client-side web application. The web application powers a blog site that shows a list of blog posts, displays a specific blog post, organizes posts by tags, and renders navigation. Combined with Kontent, each blog post is a content item and each tag is a taxonomy term.

    Client-side apps can't receive webhooks notifications when you update your content because they run on your customers' devices, not on a server. For this reason, you need to implement time-based cache invalidation. To improve performance, set shorter expiration times for the types of content that change often, like articles, and longer times for static content, such as navigation.

    Define your app's business layer

    It is a good practice to define specific actions within your app (such as retrieving a blog post) and rely on those custom actions throughout your app. Think about the actions that your app performs regularly and create methods for them.

    Using the blog site example, these actions can be:

    • Get a blog post
    • Get a list of blog posts
    • Get a list of blog posts by category

    In pseudocode, the actions might look like this:

    • JavaScript
    // Using the Delivery client directly: Client = DeliveryClient("<YOUR_PROJECT_ID>") response = Client.items() .type("<type_codename>") .limit(10) .skip(20) // Using your custom actions: // Retrieves a blog post by its codename GetPost("<post_codename>") // Retrieves a list of blog posts, also used for paging GetPosts( <skip>, <limit>) // Retrieves a list of blog posts tagged with a specific category, also used for paging GetPostsByCategory("<category_codename>", <skip>, <limit>)
    // Using the Delivery client directly: Client = DeliveryClient("<YOUR_PROJECT_ID>") response = Client.items() .type("<type_codename>") .limit(10) .skip(20) // Using your custom actions: // Retrieves a blog post by its codename GetPost("<post_codename>") // Retrieves a list of blog posts, also used for paging GetPosts( <skip>, <limit>) // Retrieves a list of blog posts tagged with a specific category, also used for paging GetPostsByCategory("<category_codename>", <skip>, <limit>)

    One of the benefits of using your own custom actions is that you can use a combination of the action's name and input parameters to name the response (in other words, compose a cache entry key) and store it in the cache.

    Implement logic for caching your content

    When you retrieve a blog post using your custom GetPost() action, you get a JSON response with the specified blog post (content item) and possibly several other content items linked to the blog post. 

    In simple terms, whenever you get a response from the API, you store it in the cache. To do this, create a cache entry that uses a naming pattern such as <action_name>|<action_parameters> for its key. Use this pattern to uniquely identify the responses within the cache for each of your custom actions.

    For instance, if you retrieve a blog post named My blog post (which translates into calling GetPost("my_blog_post")), you name the cache entry key for the response GetPost|my_blog_post. You can adjust the pattern to suit your needs and naming conventions.

    Once you put the response in the cache, you're done. The next time your app calls GetPost("my_blog_post"), it retrieves the blog post from the cache without having to make any request to the API.

    Apps with content always up to date

    If you need your app's content to stay always up to date, you'll need to use webhooks and more advanced logic. Your app needs to identify dependencies in the API responses and invalidate cache entries based on information in the webhook notifications.

    Set up a server-side proxy to handle the webhook notifications and invalidate the cache entries for your client app. All instances of the client app request content from the server-side proxy. Since the proxy is server-side and can receive webhook notifications (unlike the client apps), you can ensure that it always has the latest content. Your client-side app then gets this latest content from the proxy and serves it to your customers.

    The proxy is the only client that gets content from the Delivery API so it needs to be always available.

    Combine the approaches

    For better performance, the caching should happen also on the level of the client app. In the case of the client app caching, use the time-based approach.

    With this setup, your client app uses a time-based cache and when your proxy receives a notification about content change, it notifies your client app using a notification mechanism similar to Kontent webhooks, such as SignalR.

    Caching for mobile apps

    The techniques for caching in mobile apps are very similar to caching in web apps:

    • Cache content directly in the mobile app and use time-based cache invalidating. 
    • Set up a server-side proxy to handle the caching for your mobile app.

    Basic mobile apps with time-based caching

    If you're OK with content being outdated for 10 or 20 minutes, you can cache content only in your mobile client. This is also suitable when you don't have the resources to maintain a proxy server to handle webhooks. With this approach, your mobile app gets content directly from the Delivery API. Dive deeper into the client-side caching.

    Mobile apps with always current content

    If you're building a mobile app where up-to-date content is critical, use webhooks. This approach is more complex but it enables you to ensure the content in your app is always current.

    In this scenario, you use a server-side app that receives the webhook notifications and handles the cache invalidation for your mobile client app.

    Handle cache dependencies

    For example purposes, let's assume you get an API response that contains a few components and links to several content items. The linked items are dependencies of the blog post. If the dependencies change, so should the blog post. Let's now go through how you can identify the dependencies and store them in the cache.

    The structure of the response looks similar to the simplified JSON below.

    • The blog post itself is within the item object.
    • Components and linked items are within the modular_content collection as separate objects.
    • JSON
    { "item": { "system": { "id": "f4b3fc05-e988-4dae-9ac1-a94aba566474", "name": "My blog post", "codename": "my_blog_post", "language": "default", "type": "blog_post", "sitemap_locations": [], "last_modified": "2022-10-20T12:03:48.4628352Z" }, "elements": { ... } }, "modular_content": { "other_blog_post": { "system": { ... }, "elements": { ... } }, "n2dfcbed2_d7a1_0183_4324_a2282f735f48": { "system": { ... }, "elements": { ... } } } }
    { "item": { "system": { "id": "f4b3fc05-e988-4dae-9ac1-a94aba566474", "name": "My blog post", "codename": "my_blog_post", "language": "default", "type": "blog_post", "sitemap_locations": [], "last_modified": "2022-10-20T12:03:48.4628352Z" }, "elements": { ... } }, "modular_content": { "other_blog_post": { "system": { ... }, "elements": { ... } }, "n2dfcbed2_d7a1_0183_4324_a2282f735f48": { "system": { ... }, "elements": { ... } } } }

    You need to go through the response, find any content items, and add them as dependencies for the cached JSON response.

    Identify content items and components in modular_content

    When you retrieve content items from the Delivery API, the modular_content collection contains both content items and components. Structurally, components and content items look the same in the API response.

    To identify content items and components in the response, find the object's ID and check the third group of characters in the ID.

    • If the characters do NOT start with 01, for example, ce0288e7-294c-46e5-b9bc-b086656d5c48, it's a content item.
    • If the characters start with 01, it's a component.

    To uniquely identify the cache dependencies, use a naming pattern such as <object_type>:<object_codename>. Using the content item from the simplified JSON, the dependency name would be item:other_blog_post. This way, you can invalidate the correct dependencies whenever you receive a notification about changes in content items.

    The same principle applies to other types of responses that contain a list of items or taxonomy groups. Use the following guidelines for constructing the cache dependency logic in your app.

    Guidelines on cache dependencies

    For an API response with:

    • Single content item
      • If the number of objects in the modular_content property is below 30, add cache dependencies for all the objects.
      • If the modular_content property contains too many objects to define as separate dependencies, add a special general dependency on any content item. In such case, the cached response will be invalidated whenever any other item is invalidated.
    • List of content items, add cache dependencies for all the items in the list.
    • Single taxonomy group, add a cache dependency for the taxonomy group object returned within the response.
    • List of taxonomy groups, add cache dependency for all the taxonomy groups in the list.

    Once you're done adding the logic, you need to specify when each dependency should be invalidated.

    Guidelines on invalidating cache dependencies

    Use the following guidelines for specifying which cache dependencies must be invalidated after your app receives a webhook notification.

    Your app invalidates cache entries based on what the webhook notification tells you. Here's what to do when the notification is about a change in:

    • Content item – Pair the content item in the notification with its cache dependencies and invalidate the dependencies. Invalidate also cached lists of content items.
      • For example, if you're caching paged responses, the removal of one content item from the first response would affect all the following paged responses.
    • Taxonomy group – Pair the taxonomy group with its cache dependencies, such as content items tagged with the terms from the group, and invalidate them. Invalidate also cached lists of any objects. Taxonomy groups may be used in any content type and content item, you need to refresh any cached lists of these objects.
    • Content type – Invalidate all cache dependencies of content items based on the type.

    Webhook notifications from Kontent consist of two objects, Data and Message. The Data object specifies which entities in your project changed. The Message object tells you why the notification came and contains additional metadata about the notification.

    • JSON
    { "data": { "items": [ { "id": "e5d575fe-9608-4523-a07d-e32d780bf92a", "codename": "other_blog_post", "language": "default", "type": "blog_post" } ], "taxonomies": [ { "id": "4794dde6-f700-4a5d-b0dc-9ae16dcfc73d", "codename": "tags" } ] }, "message": { ... } } }
    { "data": { "items": [ { "id": "e5d575fe-9608-4523-a07d-e32d780bf92a", "codename": "other_blog_post", "language": "default", "type": "blog_post" } ], "taxonomies": [ { "id": "4794dde6-f700-4a5d-b0dc-9ae16dcfc73d", "codename": "tags" } ] }, "message": { ... } } }

    To use the notifications for cache invalidation, your app needs to go through the arrays of items and taxonomies in the Data object. Each array contains objects that specify the modified content item or taxonomy group. Using these objects' codenames, you can construct identifiers of the specific dependencies and invalidate them.

    For example, if you receive a notification with an object in the items array and that object's codename is other_blog_post, you'll invalidate a cache dependency identified as item:other_blog_post. This in turn invalidates any JSON responses dependent on the modified content item.

    What's next?