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
andcacheLife
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.