Utilities
Cache API
This page takes you on a tour of caching in the browser with Remix PWA and the utilities provided
Caching is a crucial part of a good Progressive Web App. It allows you to store assets and data in the browser for later use, reducing the need to fetch them from the network. This can lead to faster load times, reduced data usage, and a better user experience.
In the browser, there are several key-value stores you can use for caching. There is the Session Storage which is cleared when the browser is closed, Local Storage which persists even after the browser is closed, IndexedDB which is a more powerful database-like storage, and the Cache API which is specifically designed for caching requests/responses.
We would be focusing specifically on Cache API in this doc, so sit back, relax, make sure you have a warm cup of tea 🍵 and let's dive in.
Strategies
Strategies in Remix PWA are a way to define how a cache should be used when fetching a resource. In other words, they are a wrapper around a basic browser cache that supercharges it with control and most importantly, behaviour.
Let's break this down a bit more. When you fetch a resource, you can choose to cache it in different ways. You can cache it only if it's not already in the cache, you can cache it and update the cache with the new response, you can cache it and update the cache in the background, etc. You can even decide not to go to the server and utilise your cache as a one-stop pitstop. These are all behaviours. These are what strategies are all about. They define how a cache should be used when fetching a resource.
Diving into Remix PWA strategies, there are 4 main strategies you can use:
CacheFirst
NetworkFirst
StaleWhileRevalidate
CacheOnly
Why did we say main strategies? Because you can also create your own custom strategies! We would get to that soon enough.
BaseStrategy
BaseStrategy
is the base class for all strategies in Remix PWA. It is an abstract class that defines a few utilities and provides a common function that must be implemented by all strategies.
The methods provided are:
async openCache(): Promise<Cache>
: This method is used to open the cache that the strategy will use (which is provided during instantiation). It returns a promise that resolves to the cache (ofthis
object).ensureRequest(request: RequestInfo | URL): Request
: This utility method is used to ensure that the request is aRequest
object. If it's a string or URL, it converts it to aRequest
object.async cleanupCache(): Promise<void>
: This method is used to clean up the cache based on pre-defined parameters. It returns a promise that resolves when the cleanup is done.addTimestampHeader(response: Response): Response
: This utility method is used to add a timestamp header to a response. This is used by thecleanupCache
method to determine the age of an item in the cache.handleRequest(request: Request): Promise<Response>
: The abstract method ofBaseStrategy
. This method is used to handle the request and return a response. It must be implemented by all strategies.
You should know!
Note that they are all protected except for handleRequest
. This is because they are meant to be used internally by the strategies and not by external code.
Cache Cleanup
Cache cleanup is a crucial part of caching. It helps to ensure that the cache doesn't grow too large and that old, unused resources are removed. In Remix PWA, cache cleanup is handled via the cleanupCache
method of the BaseStrategy
class.
Generally, the parameters for cleanup are based on two factors:
- The maximum number of items in the cache
- The maximum allowed age for an item in the cache
We say generally because strategies can extend and define their own parameters for a cleanup. Or make cleanups redundant. We would be exploring how exactly this works as we explore strategies on a per-strategy basis.
Type Signature
It's important to note that the BaseStrategy
class is an abstract class and cannot be instantiated directly. But it does have a constructor that is then used by the strategies that extend it. The constructor has the following signature:
export interface CacheOptions {
maxAgeSeconds?: number;
maxEntries?: number;
ignoreRoutes?: string[] | RegExp[];
matchOptions?: CacheQueryOptions;
}
new BaseStrategy(cacheName: string, options?: CacheOptions)
where:
cacheName
is the name of the cache that the strategy will useoptions
is an optional object that can contain the following properties:maxAgeSeconds
: The maximum age (in seconds) of an item in the cache. If an item is older than this, it will be removed during cleanup.maxEntries
: The maximum number of items in the cache. If the cache has more items than this, the oldest items will be removed during cleanup.ignoreRoutes
: An array of routes to ignore when caching. This is useful for unique routes that should not be cached and handled specially instead.matchOptions
🆕: An object that defines the match options for the cache. This can be used to specify how the cache should match requests.
CacheFirst
The CacheFirst
strategy is a simple strategy that fetches the resource from the cache first and then falls back to the network if the resource is not in the cache. This is useful for resources that are expected to be in the cache most of the time.
The CacheFirst
strategy has the following signature:
type CacheableResponseOptions = {
statuses?: number[];
headers?: Record<string, string>;
};
interface CacheFriendlyOptions extends CacheOptions {
cacheableResponse?: CacheableResponseOptions | false;
}
new CacheFirst(cacheName: string, options?: CacheFriendlyOptions)
where:
cacheName
is the name of the cache that the strategy will useoptions
is an optional object that can contain the following properties:maxAgeSeconds
: The maximum age (in seconds) of an item in the cache. If an item is older than this, it will be removed during cleanup.maxEntries
: The maximum number of items in the cache. If the cache has more items than this, the oldest items will be removed during cleanup.cacheableResponse
: An object that defines the cacheable response options. This can be used to specify which status codes and headers should be cached (a status & headers filter). If set tofalse
, all responses would be cached. (Default: false)
This is the first strategy we see that extends BaseStrategy
parameters. It adds a cacheableResponse
parameter that allows you to specify which status codes and headers should be cached.
NetworkFirst
The NetworkFirst
strategy is the opposite of the CacheFirst
strategy. It fetches the resource from the network first and then falls back to the cache if the network request fails. This is useful for resources that are expected to change frequently and should always be up-to-date.
The NetworkFirst
strategy has the following signature:
type CacheableResponseOptions = {
statuses?: number[];
headers?: Record<string, string>;
};
interface NetworkFriendlyOptions extends CacheOptions {
networkTimeoutInSeconds?: number;
cacheableResponse?: CacheableResponseOptions | false;
}
new NetworkFirst(cacheName: string, options?: NetworkFriendlyOptions)
where:
cacheName
is the name of the cache that the strategy will useoptions
is an optional object that can contain the following properties:maxAgeSeconds
: The maximum age (in seconds) of an item in the cache. If an item is older than this, it will be removed during cleanup.maxEntries
: The maximum number of items in the cache. If the cache has more items than this, the oldest items will be removed during cleanup.networkTimeoutInSeconds
: The timeout (in seconds) for the network request. If the network request takes longer than this, it would time out and attempt to fallback to cache, the cache will be used instead. (Default: 10)cacheableResponse
: An object that defines the cacheable response options. This can be used to specify which status codes and headers should be cached (a response status & headers filter). If set tofalse
, all responses would be cached.
StaleWhileRevalidate
The StaleWhileRevalidate
strategy is a hybrid strategy that fetches the resource from the cache first and then fetches it from the network in the background. This is useful for resources that are expected to be in the cache most of the time but need to be updated periodically.
The StaleWhileRevalidate
strategy has the following signature:
interface SWROptions extends CacheOptions {}
new StaleWhileRevalidate(cacheName: string, options?: SWROptions)
In this case, the StaleWhileRevalidate
strategy does not have any additional parameters beyond the BaseStrategy
parameters.
StaleWhileRevalidate
also has the least agressive cache cleanup and validation, as it is expected to be used for resources that are expected to be in the cache most of the time.
Regarding
SWROptions
, I am still exploring extra features to inject into this strategy. If you have any ideas, feel free to share them with me.Currently considering a notification system for when the cache is updated, and a way to define the revalidation interval, but not how useful they would be.
CacheOnly
The CacheOnly
strategy is the most aggressive caching strategy. It fetches the resource from the cache and does not make a network request at all. This is useful for resources that are expected to be in the cache all the time and should not be updated from the network.
The CacheOnly
strategy has the following signature:
interface CacheFriendlyOptions extends CacheOptions {
cacheableResponse?: CacheableResponseOptions | false;
}
new CacheOnly(cacheName: string, options?: CacheFriendlyOptions)
where:
cacheName
is the name of the cache that the strategy will useoptions
is an optional object that can contain the following properties:maxAgeSeconds
: The maximum age (in seconds) of an item in the cache. If an item is older than this, it will be removed during cleanup.maxEntries
: The maximum number of items in the cache. If the cache has more items than this, the oldest items will be removed during cleanup.cacheableResponse
: An object that defines the cacheable response options. This can be used to specify which status codes and headers should be cached (a response status & headers filter). If set tofalse
, all responses would be cached.
CacheOnly
also exposes a method asides handleRequest
, and that's the putInCache
method. This method is used to put a response in the cache. It has the following signature:
putInCache(request: Request, response: Response): Promise<void>
The difference between this and a normal put
is that the response in this case is given a special timestamp header that allows cleanup to occur.