Mastering Data Caching in Next.js 15 with the dynamicIO API

Next.js 15 represents a significant shift in how data caching is handled, moving away from aggressive default caching towards a more developer-controlled approach. This change empowers developers with more control over performance and data freshness. This blog post will help developers by understanding how to use the new caching approach.

The Evolution of Caching in Next.js

Previous versions of Next.js were known for their aggressive caching of API calls, prioritizing performance. While beneficial in many cases, this could lead to unexpected behavior, such as displaying stale data. Developers often had to resort to workarounds like no-store or force-dynamic to bypass the cache when fresh data was required.

Next.js 15 addresses this by making uncached requests the default behavior for fetch. This means that server-side fetch calls now return fresh data every time, eliminating unexpected caching surprises.

const articles = await fetch("http://localhost:1337/api/articles").then((res) =>
  res.json(),
);

In this example, articles will always contain the latest data from the API endpoint.

Introducing the dynamicIO API

While uncached requests are the new default, Next.js 15 doesn’t abandon caching. Instead, it introduces a more refined approach through the dynamicIO API. This API (currently available in the canary version of Next.js) provides a set of tools to fine-tune caching behavior, giving developers granular control over what gets cached and for how long.

To use the dynamicIO API, install the canary version of Next.js:

npm i next@canary

Then, enable the dynamicIO flag in your next.config.js file:

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    dynamicIO: true,
  },
};

export default nextConfig;

The dynamicIO API introduces several key features, including cacheLife, cacheTag, and the 'use cache' directive, all designed to manage data caching within server components.

Setting Up a Backend API with Strapi

To illustrate these caching techniques, We can use a headless CMS like Strapi. Strapi is a powerful, open-source, headless CMS that makes it easy to create and manage APIs.

To get started, install Strapi :

npx create-strapi@latest my-strapi-project

Follow the prompts during installation. Choose Yes when asked to start with an example structure and data. This will pre-populate your Strapi instance with sample data, including articles, authors, and categories, perfect for demonstration purposes.

After installation, build the Strapi admin panel:

npm run build

Then, start the Strapi server:

npm run develop

Access the Strapi admin panel at http://localhost:1337/admin` and create an administrator account. Once logged in, navigate to the **Content Manager** to view the pre-populated data. Ensure that your articles are published to make them accessible via the API. You can access the articles data via the API endpoint:http://localhost:1337/api/articles`.

The ‘use cache’ Directive: Defining Caching Context

The 'use cache' directive is a core component of the dynamicIO API. It provides a simple and declarative way to control caching behavior, much like 'use client' and 'use server' define execution contexts. The core principle is to define the context of your caching strategy.

It’s important to note that 'use cache' is not a native React function; it’s part of the Next.js dynamicIO API.

Deep Dive into the dynamicIO API

The dynamicIO API is designed around the concept of a “composed” framework. This means you can optimize your application by caching specific components, data fetches, or even individual code blocks.

Consider the following code example:

async function getArticles() {
  try {
    const res = await fetch("http://127.0.0.1:1337/api/articles");
    if (!res.ok) throw new Error("Failed to fetch articles");
    const data = await res.json();
    return data.data || [];
  } catch (error) {
    console.error(error);
    return [];
  }
}

async function ArticlesList() {
  const articles = await getArticles();

  return (
    <div>
      {articles.length > 0 ? (
        articles.map((article) => <p key={article.id}>{article.title}</p>)
      ) : (
        <p>No articles found.</p>
      )}
    </div>
  );
}

export default function Home() {
  return (
    <div>
      <h1>/articles</h1>
      <ArticlesList />
    </div>
  );
}

With dynamicIO enabled, the getArticles function performs a purely dynamic fetch. The data is not cached by default.

Dynamic Data and Suspense

When dynamicIO is enabled, Next.js expects dynamic components (those fetching data at runtime) to be wrapped in a Suspense boundary. This is crucial for handling the asynchronous nature of data fetching. If you don’t use Suspense, you’ll encounter an error.

To fix this, import Suspense from React and wrap your dynamic component:

import { Suspense } from "react";

export default function Home() {
  return (
    <div>
      <h1>/articles</h1>
      <Suspense fallback={<div>Loading articles...</div>}>
        <ArticlesList />
      </Suspense>
    </div>
  );
}

The fallback prop provides a loading state while the data is being fetched. This ensures a smooth user experience.

Caching Strategies with Next.js

The 'use cache' directive provides different levels of caching control:

1. File-Level Caching

You can cache an entire page at the file level, making it fully static. Place the 'use cache' directive at the top of your file:

"use cache"; // Marks the page as fully static

async function getArticles() {
 // ... (same as before)
}

async function ArticlesList() {
  // ... (same as before)
}

export default function Home() {
 // ... (same as before, without Suspense)
}

With this, the entire page, including the result of getArticles, is cached. The page will not reflect any changes made in Strapi until the cache is manually invalidated.

2. Component-Level Caching

For more granular control, you can cache individual components. This allows you to create a hybrid approach, caching static parts of your UI while keeping dynamic sections up-to-date.

// Cached Sidebar (Categories)
async function Sidebar() {
  "use cache"; // Cache categories

  const categories = await getCategories();

  return (
    <aside>
        <h2>Categories</h2>
        <ul>
            {categories.map((category) => (
              <li key={category.documentId}>
                {category.name}
              </li>
            ))}
        </ul>
    </aside>
  );
}
//Dynamic Article List
async function ArticlesList() {
    const articles = await getArticles();

  return (
    <div>
      {articles.length > 0 ? (
        articles.map((article) => <p key={article.id}>{article.title}</p>)
      ) : (
        <p>No articles found.</p>
      )}
    </div>
  );
}

// Page Component
export default async function Home() {
  return (
    <div>
      <Sidebar />
      <div>
        <h1>Articles</h1>
        <Suspense fallback={<div>Loading articles...</div>}>
          <ArticlesList />
        </Suspense>
      </div>
    </div>
  );
}

In this example, the Sidebar component (fetching categories) is cached, while the ArticlesList component remains dynamic. The ArticlesList is still wrapped in Suspense because it performs a dynamic fetch.

3. API Response-Level Caching

You can also cache the result of an API call directly, which is particularly useful for expensive or slow operations.

export async function getCategories() {
  'use cache'; // Cache the API response

  const res = await fetch("http://localhost:1337/api/categories");
  if (!res.ok) throw new Error("Failed to fetch categories");

  const data = await res.json();
  return data.data || [];
}

Now, subsequent calls to getCategories will reuse the cached response until it’s revalidated.

Revalidating the Cache

The dynamicIO API provides two main ways to revalidate cached data:

1. cacheTag and revalidateTag (On-Demand Revalidation)

This method allows you to invalidate the cache for a specific piece of data whenever it changes. You first “tag” the cached data using unstable_cacheTag (imported as cacheTag):

import { unstable_cacheTag as cacheTag } from "next/cache";

export async function getCategories() {
  "use cache";
  cacheTag("categories-data"); // Tagging this API response

  const res = await fetch("http://127.0.0.1:1337/api/categories");
  if (!res.ok) throw new Error("Failed to fetch categories");

  const data = await res.json();
  return data.data || [];
}

Then, you can revalidate the cache using revalidateTag:

import { revalidateTag } from "next/cache";

export async function revalidateCategories() {
  revalidateTag("categories-data"); // Invalidates the cached categories
}

You would typically call revalidateCategories after an update in your data source (e.g., when a new category is added in Strapi).

2. cacheLife (Time-Based Revalidation)

cacheLife allows you to set a cache expiration time.

import { unstable_cacheLife as cacheLife } from "next/cache";

export async function getCategories() {
  "use cache";
  cacheLife("minutes"); // Automatically expires after a few minutes

  const res = await fetch("http://127.0.0.1:1337/api/categories");
    if (!res.ok) throw new Error("Failed to fetch categories");
  const data = await res.json();
    return data.data || [];
}

You can also define custom cache profiles in next.config.js:

module.exports = {
  experimental: {
    dynamicIO: true,
    cacheLife: {
      categories: {
        stale: 1800, // (Client) Serve stale cache for 30 mins
        revalidate: 600, // (Server) Refresh every 10 mins
        expire: 86400, // (Server) Max lifetime: 1 day
      },
    },
  },
};

And then use the profile name in your function:

import { unstable_cacheLife as cacheLife } from "next/cache";

export async function getCategories() {
  "use cache";
  cacheLife("categories"); // Uses the profile defined in next.config.js

    const res = await fetch("http://127.0.0.1:1337/api/categories");
    if (!res.ok) throw new Error("Failed to fetch categories");
  const data = await res.json();
    return data.data || [];
}

This provides a more structured and reusable way to manage cache lifetimes.

Conclusion

Next.js 15’s dynamicIO API and the 'use cache' directive offer a significant improvement in data caching control. By moving away from implicit caching and providing explicit tools, developers can now create more predictable and performant applications. The ability to choose between dynamic fetches, component-level caching, and API response caching, combined with powerful revalidation mechanisms, makes Next.js 15 a robust platform for building modern web applications.

Innovative Software Technology: Optimizing Your Next.js Caching Strategy for SEO

At Innovative Software Technology, we specialize in building high-performance, SEO-friendly web applications using Next.js. The new caching features in Next.js 15, particularly the dynamicIO API, are crucial for achieving optimal website speed and search engine ranking. We can help you leverage these features to:

  • Improve Core Web Vitals: Proper caching significantly reduces load times, directly impacting metrics like Largest Contentful Paint (LCP) and First Input Delay (FID), which are vital for SEO. We’ll implement strategic caching to ensure your site loads quickly and provides a smooth user experience.
  • Enhance Server Response Times (TTFB): By caching API responses and frequently accessed data, we can minimize server load and dramatically improve Time to First Byte (TTFB), a key ranking factor. Faster server response times lead to better crawlability and indexing by search engines.
  • Optimize Content Freshness: While caching improves performance, it’s essential to balance it with content freshness. We’ll implement revalidation strategies using cacheTag and cacheLife to ensure that search engines always have access to the latest content, preventing issues with stale or outdated information.
  • Implement Granular Caching: We’ll analyze your application’s data access patterns and implement a tailored caching strategy, caching only what’s necessary and keeping dynamic content fresh. This avoids unnecessary cache invalidation and ensures optimal performance.
  • Boost Your Search Engine Rankings: by using best practices for website speed and performance, the better and faster you website is the better chances you have to be ranked high in search engine, and to get better traffic
    By partnering with Innovative Software Technology, you can ensure that your Next.js application is not only fast and efficient but also optimized for search engines, driving organic traffic and maximizing your online visibility.

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.
You need to agree with the terms to proceed