next-cache-components — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited next-cache-components (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Cache Components enable Partial Prerendering (PPR) - mix static, cached, and dynamic content in a single route.
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;This replaces the old experimental.ppr flag.
With Cache Components enabled, content falls into three categories:
Synchronous code, imports, pure computations - prerendered at build time:
export default function Page() {
return (
<header>
<h1>Our Blog</h1> {/* Static - instant */}
<nav>...</nav>
</header>
);
}use cache)Async data that doesn't need fresh fetches every request:
async function BlogPosts() {
'use cache';
cacheLife('hours');
const posts = await db.posts.findMany();
return <PostList posts={posts} />;
}Runtime data that must be fresh - wrap in Suspense:
import { Suspense } from 'react';
export default function Page() {
return (
<>
<BlogPosts /> {/* Cached */}
<Suspense fallback={<p>Loading...</p>}>
<UserPreferences /> {/* Dynamic - streams in */}
</Suspense>
</>
);
}
async function UserPreferences() {
const theme = (await cookies()).get('theme')?.value;
return <p>Theme: {theme}</p>;
}use cache Directive'use cache';
export default async function Page() {
// Entire page is cached
const data = await fetchData();
return <div>{data}</div>;
}export async function CachedComponent() {
'use cache';
const data = await fetchData();
return <div>{data}</div>;
}export async function getData() {
'use cache';
return db.query('SELECT * FROM posts');
}'use cache'; // Default: 5m stale, 15m revalidate'use cache: remote'; // Platform-provided cache (Redis, KV)'use cache: private'; // For compliance, allows runtime APIscacheLife() - Custom Lifetimeimport { cacheLife } from 'next/cache';
async function getData() {
'use cache';
cacheLife('hours'); // Built-in profile
return fetch('/api/data');
}Built-in profiles: 'default', 'minutes', 'hours', 'days', 'weeks', 'max'
async function getData() {
'use cache';
cacheLife({
stale: 3600, // 1 hour - serve stale while revalidating
revalidate: 7200, // 2 hours - background revalidation interval
expire: 86400, // 1 day - hard expiration
});
return fetch('/api/data');
}cacheTag() - Tag Cached Contentimport { cacheTag } from 'next/cache';
async function getProducts() {
'use cache';
cacheTag('products');
return db.products.findMany();
}
async function getProduct(id: string) {
'use cache';
cacheTag('products', `product-${id}`);
return db.products.findUnique({ where: { id } });
}updateTag() - Immediate InvalidationUse when you need the cache refreshed within the same request:
'use server';
import { updateTag } from 'next/cache';
export async function updateProduct(id: string, data: FormData) {
await db.products.update({ where: { id }, data });
updateTag(`product-${id}`); // Immediate - same request sees fresh data
}revalidateTag() - Background RevalidationUse for stale-while-revalidate behavior:
'use server';
import { revalidateTag } from 'next/cache';
export async function createPost(data: FormData) {
await db.posts.create({ data });
revalidateTag('posts'); // Background - next request sees fresh data
}Cannot access cookies(), headers(), or searchParams inside use cache.
// Wrong - runtime API inside use cache
async function CachedProfile() {
'use cache';
const session = (await cookies()).get('session')?.value; // Error!
return <div>{session}</div>;
}
// Correct - extract outside, pass as argument
async function ProfilePage() {
const session = (await cookies()).get('session')?.value;
return <CachedProfile sessionId={session} />;
}
async function CachedProfile({ sessionId }: { sessionId: string }) {
'use cache';
// sessionId becomes part of cache key automatically
const data = await fetchUserData(sessionId);
return <div>{data.name}</div>;
}use cache: privateFor compliance requirements when you can't refactor:
async function getData() {
'use cache: private';
const session = (await cookies()).get('session')?.value; // Allowed
return fetchData(session);
}Cache keys are automatic based on:
async function Component({ userId }: { userId: string }) {
const getData = async (filter: string) => {
'use cache';
// Cache key = userId (closure) + filter (argument)
return fetch(`/api/users/${userId}?filter=${filter}`);
};
return getData('active');
}import { Suspense } from 'react';
import { cookies } from 'next/headers';
import { cacheLife, cacheTag } from 'next/cache';
export default function DashboardPage() {
return (
<>
{/* Static shell - instant from CDN */}
<header>
<h1>Dashboard</h1>
</header>
<nav>...</nav>
{/* Cached - fast, revalidates hourly */}
<Stats />
{/* Dynamic - streams in with fresh data */}
<Suspense fallback={<NotificationsSkeleton />}>
<Notifications />
</Suspense>
</>
);
}
async function Stats() {
'use cache';
cacheLife('hours');
cacheTag('dashboard-stats');
const stats = await db.stats.aggregate();
return <StatsDisplay stats={stats} />;
}
async function Notifications() {
const userId = (await cookies()).get('userId')?.value;
const notifications = await db.notifications.findMany({
where: { userId, read: false },
});
return <NotificationList items={notifications} />;
}| Old Config | Replacement |
|---|---|
experimental.ppr | cacheComponents: true |
dynamic = 'force-dynamic' | Remove (default behavior) |
dynamic = 'force-static' | 'use cache' + cacheLife('max') |
revalidate = N | cacheLife({ revalidate: N }) |
unstable_cache() | 'use cache' directive |
unstable_cache to use cacheunstable_cache has been replaced by the use cache directive in Next.js 16. When cacheComponents is enabled, convert unstable_cache calls to use cache functions:
Before (`unstable_cache`):
import { unstable_cache } from 'next/cache';
const getCachedUser = unstable_cache(async (id) => getUser(id), ['my-app-user'], {
tags: ['users'],
revalidate: 60,
});
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const user = await getCachedUser(id);
return <div>{user.name}</div>;
}After (`use cache`):
import { cacheLife, cacheTag } from 'next/cache';
async function getCachedUser(id: string) {
'use cache';
cacheTag('users');
cacheLife({ revalidate: 60 });
return getUser(id);
}
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const user = await getCachedUser(id);
return <div>{user.name}</div>;
}Key differences:
use cache generates keys automatically from function arguments and closures. The keyParts array from unstable_cache is no longer needed.options.tags with cacheTag() calls inside the function.options.revalidate with cacheLife({ revalidate: N }) or a built-in profile like cacheLife('minutes').unstable_cache did not support cookies() or headers() inside the callback. The same restriction applies to use cache, but you can use 'use cache: private' if needed.Math.random(), Date.now()) execute once at build time inside use cacheFor request-time randomness outside cache:
import { connection } from 'next/server';
async function DynamicContent() {
await connection(); // Defer to request time
const id = crypto.randomUUID(); // Different per request
return <div>{id}</div>;
}Sources:
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.