Lightning Components Best Practices: Caching Data with Storable Actions

Caching data at the client side can significantly reduce the number of server round-trips and improve the performance of your Lightning components. Using the Lightning Component Framework, you can access server data using either server actions or the Lightning Data Service. Server actions support optional client-side caching, and the Lightning Data Service is built on top of a sophisticated client caching mechanism. In this article, we focus on server actions and explore how to use them to cache the response of server method calls at the client-side and improve the performance of your application.

In Lightning terminology, a server action is an Apex method that you invoke remotely from your Lightning Component. A storable action is a server action whose response is stored in the client cache so that subsequent requests for the same server method with the same set of arguments can be accessed from that cache.

To make an action storable, you simply call its setStorable() function. For example:

var action = component.get("c.getItems");
action.setStorable();
action.setCallback(this, function(response) {
	// handle response
};
$A.enqueueAction(action);

When an action is marked as storable, the framework automatically returns the response from the client cache (if available) so that the data is immediately available to the component for display or processing. The framework might then call the server method in the background, and if the response is different, invoke the action callback function a second time.

Using storable actions, the cache behavior is controlled by two parameters set internally in the framework :

  • Expiration age: maximum age of the cached response. The cached response is discarded if it is older than the expiration age. In Lightning Experience, the expiration age is currently set to 900 seconds. This value is subject to change, and you can’t change it yourself.
  • Refresh age: maximum age for the cached response to be considered “fresh.” If the cached response is older than the refresh age (and younger than the expiration age), it is provided to the client, but the framework also invokes the server method in the background to refresh the cache. If the new response is different from the cached response, the action callback function is called a second time. In Lightning Experience, the refresh age is currently set to 30 seconds. Like the expiration age, this value is subject to change, and can’t be changed.

Storable action scenarios

Here are the possible scenarios when invoking a storable action:

Scenario 1: The response is not available in the cache (or has expired)

  1. The component calls a server method
  2. The framework checks if the response is available in the cache
  3. The response isn’t available in the cache or is expired (cached response age > expiration age)
  4. The framework calls the server method
  5. The server returns the response
  6. The framework caches the response
  7. The framework calls the action callback function providing the server response

Scenario 2: The response is available in the cache and doesn’t need to be refreshed

  1. The component calls a server method
  2. The framework checks if the response is available in the cache
  3. The response is available, and doesn’t need to be refreshed (cached response age <= refresh age)
  4. The framework calls the action callback function, providing the cached response

There is no server round-trip in this scenario—a nice performance win!

Scenario 3: The response is available in the cache and needs to be refreshed

  1. The component calls a server method
  2. The framework checks if the response is available in the cache
  3. The response is available in the cache and needs to be refreshed (cached response age > refresh age)
  4. The framework calls the action callback function providing the cached response
  5. The framework calls the server method to get a fresh response
  6. The server returns the response
  7. The framework updates the cache with the new response
  8. If the server response is different from the cached response, the framework calls the action callback function for the second time with the updated response

What should you cache?

Caching is a trade-off between performance and data freshness. However, remember that even without caching, there is no such thing as guaranteed fresh data: when you call a service, the data may already have changed by the time the response reaches the client.

The storable actions feature mitigates the possibility of stale data with its “trust and verify” model: when the cached response is older than the refresh age (and younger than the expiration age), it is returned to the calling component, but the framework also verifies that the data is still fresh by making a call to the server in the background.

The general guideline is to cache (mark as storable) any action that is idempotent and non-mutating.

An idempotent action is an action that produces the same result when called multiple times. For example:

  • getPage(1) is idempotent and should be cached
  • getNextPage() is not idempotent and should not be cached

A non-mutating action is an action that doesn’t modify data. Never cache an action that can create, update, or delete data. For example:

  • updateAccount(sObject) is mutating and not idempotent and should not be cached

Caching the right (idempotent and non-mutating) server actions can significantly improve the performance of the overall application even if the benefits are not obvious when you look at a component and a server action in isolation. The cache spans the entire application. If a user loads page 1 where component A invokes Apex method X, and then navigates to page 2 where component B invokes the same Apex method X, the response is served from the cache (if the cached response is younger than the refresh age). Similarly, if the user navigates back to page 1, component A again gets Apex method X’s response from the cache.

Let’s consider another example: a paginated list of items with Next Page and Previous Page buttons to navigate through the list. Every time the user clicks the Next Page or Previous Page button, the component invokes the getPage(pageNumber) method in the component’s Apex controller. Marking the getPage() action as storable ensures that each page is only retrieved once from the server. Subsequent requests for the same page are served from the cache (see Tracing Performance of Actions below for a detailed breakdown of this use case).

Storable actions vs Lightning Data Service vs custom cache

Server actions allow you to access data using a traditional service approach. You implement some logic in Apex that you expose as a remotely invocable method. Storable actions allow you to cache virtually anything (whatever the server method call returns): a record, a collection of records, a composite object, a custom data structure, data returned by a callout to a third-party service, and so on.

Lightning Data Service (currently in Developer Preview) provides a managed record approach. In other words, you are not responsible for writing any data access logic (no Apex code to write). The framework is responsible for managing records: fetching them from the server when requested the first time, storing them in a highly efficient client cache, sharing them between all components that request them, and sending changes to the server. Unlike storable actions that can cache any type of response returned by an Apex method, the Lightning Data Service caches discrete Salesforce sObjects (record collections are on the roadmap).

You can also implement your own custom cache approach. As always, make sure you don’t reinvent the wheel and only use a custom cache approach when there is no standard way to implement your caching requirements in the framework. (See the Modularizing Code in Lightning Components post for strategies to implement a custom cache.)

Caching Requirements Recommended Solution
Single record Lightning Data Service
Collections of records, composite responses, custom data structures, third-party data Storable actions
Complete control over caching implementation Custom cache

Tracing performance of actions

You can use the Chrome Developer Tools to examine network traffic. It’s particularly interesting to watch when the server method is called and when it’s not (see storable action scenarios above).

You can also use the Lightning Inspector, which can provide detailed information about the characteristics of each action invocation as illustrated in this screenshot.

Of course, you can also use console.log() to measure the performance of action invocations. Note that, in this case, you can’t distinguish between time spent in the client queue, time in transit, and server execution time. For example, here is how the findAll() method is called in the PropertyTileList component in the DreamHouse sample application:

var action = component.get("c.findAll");
var page = component.get("v.page");
action.setStorable();
action.setParams({
    "page": page
});
action.setCallback(this, function(response) {
    console.log("Page %d loaded in %fms", 
        page,
        performance.now() - startTime);
    // handle response
};
var startTime = performance.now();
$A.enqueueAction(action);

Here is a screenshot of the browser console when findAll() is invoked repeatedly in response to the user clicking the Next Page and Previous Page buttons to navigate through the list:

Note that the first calls to the server to get page 1 and 2 take just under 200 milliseconds. Subsequent calls are virtually instantaneous because they are served from the cache.

Summary

Client-side data caching is one of the most impactful things you can do to improve the performance of your Lightning components, and storable actions makes it easy to implement for many use cases. Try it in your own components, and let us know the difference it makes for you.

Resources

Leave your comments...

Lightning Components Best Practices: Caching Data with Storable Actions