Caching Across React, Next.js and Serverless Applications

Hero image for Caching Across React, Next.js and Serverless Applications. Image by Arum Visuals.
Hero image for 'Caching Across React, Next.js and Serverless Applications.' Image by Arum Visuals.

In Brief

React caching is not one technique. useMemo, browser storage, query caches, HTTP caching, static generation, ISR, serverside rendering, and Redis all solve different problems. Choose the cache based on whether you are avoiding repeated UI work, reusing remote data, controlling freshness, or protecting a serverless function.

Modern web applications rely on fetching data, whether from an API, a database, or another source. The challenge is that every request takes time and consumes resources. Without caching, applications can feel sluggish, and servers can become overwhelmed by unnecessary repeated requests.

Caching helps solve this by temporarily storing data, reducing redundant fetching, and improving performance. In React, we can cache data on the client, whilst Next.js provides builtin serverside caching mechanisms. In a serverless environment, an inmemory cache may be reused by later requests to the same warm instance, but it is neither shared across instances nor guaranteed to survive. Data that must be consistently available therefore needs a shared cache such as Redis.

This is a challenge I've faced recently in a project I've been working on. The gameplay requires very fast API responses or otherwise begins to look broken, whilst the data itself can be massive and complicated. I write about how I resolved that much more in the case study, but it prompted me to write here, too.

In this article, I will explore different caching strategies for React and Next.js applications, including best practices for caching in serverless environments.


Client‑Side Caching in React

Clientside caching is one of the simplest ways to improve performance. If data does not change often, we can store it in memory or local storage instead of refetching it from an API every time.

Using State for Basic Caching

A basic approach is to store fetched data in React state (something we should all be innately familiar with) like this:

const [data, setData] = useState<string | null>(null);useEffect(() => {  const cachedData = localStorage.getItem("cachedData");  if (cachedData) {    setData(cachedData);  } else {    fetch("https://api.example.com/data")      .then((res) => res.text())      .then((fetchedData) => {        setData(fetchedData);        localStorage.setItem("cachedData", fetchedData);      });  }}, []);

Here, we first check if the data exists in localStorage. If it is not, we then fetch it and store it for future use.

Using React Query for Smarter Caching

For more advanced clientside caching, we can use React Query. The following example uses the TanStack Query version5 object form of useQuery to cache the result and decide when it becomes stale:

import { useQuery } from "@tanstack/react-query";const fetchData = async () => {  const res = await fetch("https://api.example.com/data");  return res.json();};const MyComponent = () => {  const { data, isLoading } = useQuery({ queryKey: ["data"], queryFn: fetchData, staleTime: 60000 });  if (isLoading) return <p>Loading...</p>;  return <p>{data}</p>;};

React Query allows us to hand off cache management, intelligently managing the cache and reducing unnecessary API calls whilst also ensuring fresh data when we need it.


Server‑Side Caching in Next.js

Next.js offers several builtin caching mechanisms, which we can take advantage of to reduce server load and improve application response times.

Static Site Generation (SSG) for Prebuilt Caching

If data does not change frequently, we can prebuild pages with our data at build time using Static Site Generation (SSG). This is great for pages that rarely change, like Author pages on a blog, for example:

export async function getStaticProps() {  const res = await fetch("https://api.example.com/data");  const data = await res.json();  return { props: { data } };}

Since SSG pages are prebuilt, they do not update unless the site is redeployed. This is perfect for static content but impractical for frequently changing data.

Incremental Static Regeneration (ISR)

ISR builds on SSG by allowing pages to update after deployment at defined intervals. This means we can serve pregenerated content but refresh it periodically, like this:

export async function getStaticProps() {  const res = await fetch("https://api.example.com/data");  const data = await res.json();  return { props: { data }, revalidate: 30 };}

With ISR, a request triggers page regeneration only after the revalidate time has passed, which means that users receive cached content until it is updated. In the example above, the revalidate: 30 option in the return means that the page will be regenerated every thirty seconds, keeping content uptodate whilst also reducing API calls.

Server‑Side Rendering (SSR) with Built‑In Caching

For frequently changing data, ServerSide Rendering (SSR) can fetch data on every request. However, Next.js still allows us to add caching at the HTTP level using cache headers:

export async function getServerSideProps({ res }) {  res.setHeader("Cache-Control", "public, s-maxage=60, stale-while-revalidate=30");  const response = await fetch("https://api.example.com/data");  const data = await response.json();  return { props: { data } };}

The result here is similar to ISR (but a different approach): the API response is cached for 60 seconds, serving fresh data whilst reducing redundant requests.


Caching in Serverless Environments

Why in‑Memory Storage is Only Opportunistic in Serverless Functions

Serverless platforms such as Vercel and Netlify may reuse a warm function instance, so modulelevel objects can survive for later calls handled by that instance. The platform can also create or retire instances at any time, which means that:

  • Variables stored in memory can be reused by requests that reach the same warm instance.
  • If a function is idle, it is shut down, clearing any data you may have cached.
  • There is no guarantee the same function instance will handle the next request.

For example, this inmemory cache can produce a hit on a reused instance, but cannot provide a shared or durable serverless cache:

let cache = {};export default async function handler(req, res) {  if (cache.data) {    return res.json(cache.data);  }  const response = await fetch("https://api.example.com/data");  cache.data = await response.json();  res.json(cache.data);}

A new instance starts with an empty cache object, whilst a warm instance may reuse its copy. Under concurrency there can be several independent copies, so this is a useful opportunistic optimisation only when misses and inconsistent contents are acceptable.

Using Redis for Shared Serverless Caching

When cached data must be shared across instances, we need a shared external cache with the required availability and durability configuration. This is where Redis can fit.

What is Redis?

Redis (Remote Dictionary Server) is an inmemory data store designed for lowlatency operations, with optional persistence modes. Keeping the working data in memory avoids much diskbound query work, but applicationobserved latency still includes connection handling, serialisation, network distance and command choice. Redis is therefore useful for caching API responses, session data and frequently accessed computations when it is deployed close to the functions that call it.

A Redis cache hit can be faster than repeating an origin API or database query when the saved work outweighs the Redis round trip. That depends on deployment topology because:

  • Data is stored in memory
    , avoiding disk reads and complex query processing.
  • Latency is minimal
    inside a wellprovisioned Redis server, but the client still pays network and system latency.
  • Operations are optimised for speed
    , with commands like GET and SET executing in constant time (O(1)).

Using Redis to Cache API Responses

Instead of hitting an external API every time, we can store responses in Redis and retrieve cache hits through one measured network hop:

import Redis from "ioredis";const redis = new Redis(process.env.REDIS_URL);export default async function handler(req, res) {  // Check Redis cache first  const cachedData = await redis.get("apiData");  if (cachedData) {    return res.json(JSON.parse(cachedData));  // Return cached response  }  // Fetch fresh data from the API  const response = await fetch("https://api.example.com/data");  const data = await response.json();  // Store in Redis with a 60-second expiration  await redis.set("apiData", JSON.stringify(data), "EX", 60);  res.json(data);}

Now, if multiple users request the same data within 60 seconds, instances can share the Redis value rather than each waiting for the origin API. The response time is the Redis client round trip for the deployed topology, not an intrinsic zerocost lookup.

When is Redis Faster than the Origin?

An origin API may include authentication, network hops and database work. Redis command processing can be submillisecond, but the application must also pay its network round trip. Measure both paths from the deployed function; a distant Redis service can be slower than a nearby origin. A cache hit is beneficial when it removes enough work to beat that complete path because:

  • It eliminates the need for API authentication
    on every request.
  • It avoids making database queries
    for each user.
  • It can reduce network and origin latency
    when Redis is colocated with the function and replaces a slower remote request.

By integrating Redis into a serverless architecture, we can significantly reduce response times and minimise redundant API calls, making our applications much more scalable.


Wrapping Up

Caching is crucial for optimising React and Next.js applications. Clientside caching improves performance for repeated API calls, whilst Next.js offers builtin caching mechanisms like ISR and HTTP cache headers. In serverless environments, inmemory state may be reused within a warm instance, but shared cache correctness needs an external service such as Redis and latency must be measured in the deployed topology.

Key Takeaways

  • Clientside caching

    (React Query, localStorage) reduces API calls.
  • Next.js caching

    (SSG, ISR, SSR) minimises server requests and improves load times.
  • Serverless functions can

    reuse inmemory state on a warm instance, but cannot treat it as shared or durable.
  • Redis

    provides a persistent caching layer for serverless environments.

Using the right caching strategy makes a huge difference in performance and scalability. Whether we are optimising API calls in React, caching pages in Next.js, or handling data in a serverless environment, a wellplanned approach keeps our applications fast and responsive.


Looking for technical direction?

I support teams that need senior judgement on React, Next.js, headless CMS architecture, performance, migrations, and technical SEO.