> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

# Rendering Cache

When developing applications, sometimes we cache computation results using hooks like React's `useMemo` and `useCallback`. By leveraging caching, we can reduce the number of computations, thus saving CPU resources and improving user experience.

Modern.js supports caching server-side rendering (SSR) results, reducing the computational and rendering time during subsequent requests. This accelerates page load time and improves user experience. Additionally, caching lowers server load, conserves computational resources, and speeds up user access.

## Configuration

Create a `server/cache.[t|j]s` file in your application and export the `cacheOption` configuration to enable SSR rendering cache:

```ts title="server/cache.ts"
import type { CacheOption } from '@modern-js/server-runtime';

export const cacheOption: CacheOption = {
  maxAge: 500, // ms
  staleWhileRevalidate: 1000, // ms
};
```

## Configuration Details

### Cache Configuration

The caching strategy implements [stale-while-revalidate](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control).

Within the `maxAge` period, the cache content is directly returned. Exceeding `maxAge` but within `staleWhileRevalidate`, the cache content is still returned directly, but it re-renders asynchronously.

**Object Type**

```ts
export interface CacheControl {
  maxAge: number;
  staleWhileRevalidate: number;
  customKey?: string | ((pathname: string) => string);
}
```

Here, `customKey` is the custom cache key. By default, the cache key is derived from the request's origin, normalized pathname (trailing slash removed), and query string. When `customKey` is set, it fully replaces the default key: the `customKey` callback receives the normalized pathname (not the `CacheOptionProvider`'s `req`) and is responsible for the whole partition (e.g. by tenant, host, or query) — it must not embed raw cookies or other credentials into the returned key.

**Function Type**

```ts
export type CacheOptionProvider = (
  req: IncomingMessage,
) => Promise<CacheControl | false> | CacheControl | false;
```

Sometimes, developers need to use `req` to customize the cache key, or prevent caching for specific URLs. You can configure this as a function, as shown:

```ts title="server/cache.ts"
import type { CacheOption, CacheOptionProvider } from '@modern-js/server-runtime';

const provider: CacheOptionProvider = (req) => {
  const { url, headers, ... } = req;
  if(url.includes('no-cache=1')) {
    return false;
  }

  const key = computedKey(url, headers, ...);
  return {
    maxAge: 500, // ms
    staleWhileRevalidate: 1000, // ms
    customKey: key,
  }
}

export const cacheOption: CacheOption = provider;
```

**Mapping Type**

```ts
export type CacheOptions = Record<string, CacheControl | CacheOptionProvider>;
```

Sometimes, different routes require different caching strategies. We also offer a mapping configuration method, as shown below:

```ts title="server/cache.ts"
import type { CacheOption } from '@modern-js/server-runtime';

export const cacheOption: CacheOption = {
  '/home': {
    maxAge: 50,
    staleWhileRevalidate: 100,
  },
  '/about': {
    maxAge: 1000 * 60 * 60 * 24, // one day
    staleWhileRevalidate: 1000 * 60 * 60 * 24 * 2 // two days
  },
  '*': (req) => { // If no above route matches, this applies
    const { url, headers, ... } = req;
    const key = computedKey(url, headers, ...);

    return {
      maxAge: 500,
      staleWhileRevalidate: 1000,
      customKey: key,
    }
  }
}
```

- The route `http://xxx/home` will apply the first rule.
- The route `http://xxx/about` will apply the second rule.
- The route `http://xxx/abc` will apply the last rule.

The above `/home` and `/about` are patterns, meaning `/home/abc` will also match. You can use regex in these patterns, such as `/home/.+`.

### Cache Policy

The following policy is enforced automatically and cannot be overridden by the returned `CacheControl`:

- Only `GET` requests are cached.
- A request carrying a `Cookie` or `Authorization` header skips the cache unless `customKey` is set, since a custom key is treated as an explicit partitioning contract.
- A request with `Cache-Control: private`, `no-cache`, or `no-store` skips the cache.
- A response is only stored when it is `200`, has no `private`/`no-cache`/`no-store` in `Cache-Control`, has no `Set-Cookie`, and has no nonempty `Vary` header; otherwise any existing cache entry for that key is deleted instead of being written.
- If a `stale` revalidation produces a response that is no longer cacheable (for example, it became per-user), that already-served stale response is unaffected, but the cache entry is evicted so subsequent requests no longer hit it.
- A `CacheOptionProvider` returning `false` still disables caching entirely for that request, independent of the policy above.

Cache keys are namespaced (`__ssr__cache:v2:...`). Older un-namespaced entries — including ones already stored in a custom `Container` — are never read back; they simply age out under the container's own eviction/TTL. There is no global cache flush.

### Cache Container

By default, the server uses memory for caching. Typically, services are deployed in a Serverless container, creating a new process for each access, making it impossible to use the previous cache.

Thus, Modern.js allows developers to define custom cache containers. Containers must implement the `Container` interface:

```ts
export interface Container<K = string, V = string> {
  /**
   * Returns a specified element from the container. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Container.
   * @returns Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.
   */
  get: (key: K) => Promise<V | undefined>;

  /**
   * Adds a new element with a specified key and value to the container. If an element with the same key already exists, the element will be updated.
   *
   * The ttl indicates cache expiration time.
   */
  set: (key: K, value: V, options?: { ttl?: number }) => Promise<this>;

  /**
   * @returns boolean indicating whether an element with the specified key exists or not.
   */
  has: (key: K) => Promise<boolean>;

  /**
   * @returns true if an element in the container existed and has been removed, or false if the element does not exist.
   */
  delete: (key: K) => Promise<boolean>;
}
```

Developers can implement a Redis cache container as shown below:

```ts
import Redis from 'ioredis';
import type { Container, CacheOption } from '@modern-js/server-runtime';

class RedisContainer implements Container {
  redis = new Redis();

  async get(key: string) {
    return this.redis.get(key);
  }

  async set(key: string, value: string): Promise<this> {
    this.redis.set(key, value);
    return this;
  }

  async has(key: string): Promise<boolean> {
    return this.redis.exists(key) > 0;
  }

  async delete(key: string): Promise<boolean> {
    return this.redis.del(key) > 0;
  }
}

const container = new RedisContainer();

export const customContainer: Container = container;

export const cacheOption: CacheOption = {
  maxAge: 500, // ms
  staleWhileRevalidate: 1000, // ms
};
```

## Cache Identification

When rendering cache is enabled, Modern.js identifies the cache status of the current request through the `x-render-cache` response header. Here's an example response:

```bash
< HTTP/1.1 200 OK
< Access-Control-Allow-Origin: *
< content-type: text/html; charset=utf-8
< x-render-cache: hit
< Date: Thu, 29 Feb 2024 02:46:49 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
< Content-Length: 2937
```

The `x-render-cache` header can have the following values:

| Name    | Description                                                                         |
| ------- | ----------------------------------------------------------------------------------- |
| hit     | Cache hit, returned cache content                                                   |
| stale   | Cache hit, but data is stale, returned cache content and re-rendered asynchronously |
| expired | Cache expired, re-rendered and returned new content                                 |
| miss    | Cache missed                                                                        |
