404 Not Found

404 Not Found


nginx

Data Retrieval: The Core Caching Mechanisms of fetch and RSC

In RSC, fetch is no longer just a browser fetch—it extends the cache layer, allowing you to control the data lifecycle in a declarative way.

1. What You'll Learn


2. A True Story of a Full-Stack Developer

(1) Pain Point: The dashboard takes 8 seconds to load

Bob is the Technical Lead of the TaskFlow team. The Dashboard page needs to load five data sources: user statistics, total number of projects, recent tasks, activity logs, and system notifications. The initial code used five serial await fetch(...) calls, each waiting for the previous one to complete—resulting in a total time of 2.1s + 1.8s + 1.5s + 0.9s + 1.7s = 8 seconds. Users complained that the page took "too long to load." To make matters worse, the API was re-queried with every refresh, causing database load to spike to 5,000 QPS.

(2) The Next.js fetch Solution

Use Promise.all() for parallel requests + next: { revalidate: 60 } for a 60-second cache.

TSX
// app/dashboard/page.tsx
export default async function DashboardPage() {
  const [users, projects, tasks, logs, notifs] = await Promise.all([
    fetch('https://api.example.com/stats/users', { next: { revalidate: 60 } }),
    fetch('https://api.example.com/stats/projects', { next: { revalidate: 60 } }),
    fetch('https://api.example.com/stats/tasks', { next: { revalidate: 30 } }),
    fetch('https://api.example.com/activity/logs', { cache: 'no-store' }),
    fetch('https://api.example.com/notifications', { next: { revalidate: 10 } }),
  ]).then(responses => Promise.all(responses.map(r => r.json())))

  return <DashboardView {...{ users, projects, tasks, logs, notifs }} />
}

(3) Revenue

Dimension Before Optimization After Optimization
Page load time 8 seconds (serial) 2.1 seconds (parallel)
Database QPS 5,000 83 (60-second cache)
User Complaints 12 per day 0
Number of lines of code 35 lines (5 separate fetches) 10 lines

3. The Three Caching Modes of fetch

Next.js 16 extends the Web fetch API by adding three caching modes. All fetch in RSC use force-cache (auto-caching) by default, unless another mode is explicitly specified.

100%
graph LR
    A[RSC fetch] --> B{Caching Mode}
    B --> C[force-cache<br/>Default value]
    B --> D[no-store<br/>Every time the page is refreshed]
    B --> E[revalidate:N<br/>Time Window]
    C --> F[Data Cache<br/>Persistent Storage]
    D --> G[Real-time Data<br/>Do not cache]
    E --> H[N Cached within seconds<br/>Re-obtain after expiration]
    
    style C fill:#d4edda
    style D fill:#f8d7da
    style E fill:#fff3cd
Pattern Syntax Behavior Use Cases
force-cache (default) fetch(url) or fetch(url, { cache: 'force-cache' }) Retrieved only during build or on the first request; results are cached permanently Data that rarely changes (documents, static configuration)
no-store fetch(url, { cache: 'no-store' }) Retrieve data anew with every request; no caching Real-time data (user information, inventory)
revalidate:N fetch(url, { next: { revalidate: 60 } }) Cached for 60 seconds; the background process triggers an update upon expiration Semi-real-time data (news, leaderboards)

(1) force-cache Default Behavior

If no options are passed, Next.js automatically caches fetch results—requests with the same URL and options are made only once during the build process.

TSX
// app/products/page.tsx — force-cache Default
export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products').then(r => r.json())
  // Retrieve once during build time,Use the cache afterward
  return <ProductList data={products} />
}

(2) no-store Dynamic Data

TSX
// app/profile/page.tsx — Retrieve the latest data with every request
export default async function ProfilePage() {
  const user = await fetch('https://api.example.com/me', {
    cache: 'no-store',
    headers: { Authorization: `Bearer ${process.env.API_TOKEN}` }
  }).then(r => r.json())
  return <ProfileView user={user} />
}

(3) revalidate Time Window

TSX
// app/blog/[slug]/page.tsx — ISR Style Cache
export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await fetch(`https://cms.example.com/posts/${params.slug}`, {
    next: { revalidate: 3600 }  // 1 Use the cache within an hour
  }).then(r => r.json())
  return <article><h1>{post.title}</h1><div>{post.content}</div></article>
}

▶ Example: Comparison of Three Caching Modes (Difficulty: ⭐)

TSX
// app/cache-demo/page.tsx
export default async function CacheDemoPage() {
  const staticData = await fetch('http://worldtimeapi.org/api/timezone/Etc/UTC', {
    cache: 'force-cache'
  }).then(r => r.json())

  const liveData = await fetch('http://worldtimeapi.org/api/timezone/Etc/UTC', {
    cache: 'no-store'
  }).then(r => r.json())

  return (
    <div>
      <p>Static (force-cache): {staticData.datetime}</p>
      <p>Live (no-store): {liveData.datetime}</p>
    </div>
  )
}

Output:

TEXT
Static (force-cache): 2026-07-06T10:00:00.000Z  ← Always the Same
Live (no-store): 2026-07-06T10:00:05.123Z       ← It changes every time I refresh

4. On-Demand Revalidation: tags and revalidateTag

next: { tags: [...] } Tag the fetch request, then use revalidateTag(tag) to refresh the cache as needed in the Server Action or Route Handler.

100%
sequenceDiagram
    participant A as Server Action
    participant Cache as Data Cache
    participant DB as Database

    A->>DB: Write New Data(Create a Task)
    A->>Cache: revalidateTag('tasks')
    Cache->>Cache: Clear tags All caches that match
    Note over Cache: Next time fetch Retrieve Again
API Purpose Where to Call
next: { tags: ['tasks', 'projects'] } Tagging "fetch" fetch() Options
revalidateTag('tasks') Clear all related cache by tag Server Action / Route Handler
revalidatePath('/dashboard') Clear cache by path Server Action / Route Handler

▶ Example: Using tags and revalidateTag (Difficulty: ⭐⭐)

TSX
// app/tasks/data.ts — Data Retrieval Functions
export async function getTasks() {
  return fetch('https://api.example.com/tasks', {
    next: { tags: ['tasks'] }
  }).then(r => r.json())
}
TSX
// app/tasks/actions.ts — Server Action Refresh the cache after writing
'use server'
import { revalidateTag } from 'next/cache'

export async function createTask(formData: FormData) {
  const title = formData.get('title') as string
  await fetch('https://api.example.com/tasks', {
    method: 'POST',
    body: JSON.stringify({ title, status: 'todo' })
  })
  revalidateTag('tasks')  // Clear tasks All cached entries for this tag
}

▶ Example: Using revalidatePath to clear the entire page (Difficulty: ⭐⭐)

TSX
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'

export async function publishArticle() {
  await db.article.update({ where: { id: 1 }, data: { published: true } })
  revalidatePath('/blog')       // Refresh /blog Page
  revalidatePath('/blog/[slug]') // Refresh all article details
}

5. Parallel Data Retrieval and Avoiding Waterfall Effects

The waterfall pattern is the number one performance killer—each await waits in sequence for the previous one to complete. Using Promise.all() allows all requests to be initiated simultaneously.

100%
graph LR
    subgraph "Waterfall Pattern (Slow)"
        A1[fetch A] --> A2[fetch B] --> A3[fetch C]
        A1 -.- t1[2s]
        A2 -.- t2[+2s = 4s]
        A3 -.- t3[+2s = 6s]
    end
    subgraph "Parallel (Fast)"
        B1[fetch A] -.- u1[2s]
        C1[fetch B] -.- u2[2s]
        D1[fetch C] -.- u3[2s]
        B1 & C1 & D1 --> M[Promise.all<br/>Total Time ~2s]
    end
Pattern Implementation Total Time (2 seconds each) Applicable Scenarios
Serial Waterfall await A; await B; await C ~6s Dependent requests
Parallel Requests Promise.all([A, B, C]) ~2s Independent, unrelated requests
Phased parallel processing const a = await A; const [b, c] = await Promise.all([B(a.id), C]) ~4s Partially dependent requests

▶ Example: Serial Waterfall Pattern Recognition (Difficulty: ⭐)

TSX
// app/waterfall/page.tsx — ❌ Serial Waterfall Chart
export default async function WaterfallPage() {
  const user = await fetch('https://api.example.com/user').then(r => r.json())          // 1s
  const tasks = await fetch(`https://api.example.com/tasks?userId=${user.id}`).then(r => r.json())  // Wait until that's done + 2s = 3s
  const details = await Promise.all(tasks.map(t =>
    fetch(`https://api.example.com/tasks/${t.id}/details`).then(r => r.json())  // Wait until that's done + 2s = 5s
  ))

  return <div>Total: ~5s</div>
}

▶ Example: Parallel Optimization (Difficulty: ⭐⭐)

TSX
// app/no-waterfall/page.tsx — ✅ Parallel Optimization
export default async function NoWaterfallPage() {
  // Stage 1:Concurrently Retrieve Users and Initialize Data
  const [user, initialData] = await Promise.all([
    fetch('https://api.example.com/user', { next: { revalidate: 10 } }).then(r => r.json()),
    fetch('https://api.example.com/initial', { cache: 'no-store' }).then(r => r.json()),
  ])

  // Stage 2:Dependency user.id Request(There are still small waterfalls,But it's already the best)
  const tasks = await fetch(`https://api.example.com/tasks?userId=${user.id}`).then(r => r.json())

  return <div>Total: ~2s (1s + 1s parallel, then 1s)</div>
}

▶ Example: Suspense Lazy Loading (Difficulty: ⭐⭐⭐)

TSX
// app/suspense-demo/page.tsx — Each separate area uses Suspense Package
import { Suspense } from 'react'

export default function SuspenseDemoPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<div>Loading profile...</div>}>
        <ProfileSection />
      </Suspense>
      <Suspense fallback={<div>Loading tasks...</div>}>
        <TaskSection />
      </Suspense>
    </div>
  )
}

async function ProfileSection() {
  const user = await fetch('https://api.example.com/user', { cache: 'no-store' }).then(r => r.json())
  return <div>Welcome, {user.name}</div>
}

async function TaskSection() {
  const tasks = await fetch('https://api.example.com/tasks', { next: { revalidate: 30 } }).then(r => r.json())
  return <ul>{tasks.map((t: any) => <li key={t.id}>{t.title}</li>)}</ul>
}

6. Complete Example: Optimized Dashboard

TSX
// app/dashboard-optimized/page.tsx
import { Suspense } from 'react'
import { revalidateTag } from 'next/cache'

// ======== Data Functions ========
const API = 'https://jsonplaceholder.typicode.com'

async function getData<T>(endpoint: string, options?: RequestInit): Promise<T> {
  const res = await fetch(`${API}${endpoint}`, {
    ...options,
    next: { tags: [endpoint.split('/')[1] ?? 'default'], ...(options as any)?.next },
  })
  if (!res.ok) throw new Error(`Failed to fetch ${endpoint}`)
  return res.json()
}

// ======== Retrieve all data in parallel ========
export default function DashboardOptimizedPage() {
  return (
    <div>
      <h1>Optimized Dashboard</h1>
      <div style={{ display: 'grid', gap: 16, gridTemplateColumns: '1fr 1fr' }}>
        <Suspense fallback={<Skeleton label="Users" />}>
          <DataCard title="Users" endpoint="/users" />
        </Suspense>
        <Suspense fallback={<Skeleton label="Posts" />}>
          <DataCard title="Posts" endpoint="/posts" revalidate={120} />
        </Suspense>
        <Suspense fallback={<Skeleton label="Comments" />}>
          <DataCard title="Comments" endpoint="/comments" />
        </Suspense>
        <Suspense fallback={<Skeleton label="Todos" />}>
          <DataCard title="Todos" endpoint="/todos" revalidate={30} />
        </Suspense>
      </div>
    </div>
  )
}

async function DataCard({ title, endpoint, revalidate }: {
  title: string
  endpoint: string
  revalidate?: number
}) {
  const data = await getData<any[]>(endpoint, revalidate
    ? { next: { revalidate } }
    : { cache: 'no-store' }
  )
  return (
    <div style={{ border: '1px solid #ddd', borderRadius: 8, padding: 16 }}>
      <h2>{title} <span style={{ fontSize: 14, color: '#666' }}>({data.length})</span></h2>
      <ul>{data.slice(0, 5).map((item: any) => (
        <li key={item.id}>{item.title ?? item.name ?? item.email}</li>
      ))}</ul>
    </div>
  )
}

function Skeleton({ label }: { label: string }) {
  return <div style={{ border: '1px solid #eee', borderRadius: 8, padding: 16, opacity: 0.5 }}>
    Loading {label}...
  </div>
}

// app/dashboard-optimized/actions.ts
'use server'
import { revalidateTag } from 'next/cache'

export async function refreshSection(tag: string) {
  revalidateTag(tag)
  return { success: true }
}

❓ FAQ

Q Do the 'force-cache' and 'no-store' options in fetch behave the same way in development mode and production mode?
A No, they do not. In development mode (npm run dev), force-cache is still fetched with every request (to facilitate debugging). Caching only takes effect in production mode (next start or after the build). This is a design decision in Next.js—to always fetch the latest data during the development phase.
Q What is the difference between revalidateTag and revalidatePath?
A revalidateTag clears the cache by tag (for the same data across different pages), while revalidatePath clears the cache by path (down to the page or route pattern). The former is suitable for fine-grained data-layer control, while the latter is suitable for page-level refreshes. We recommend using revalidateTag whenever possible.
Q If two fetch requests use the same URL but different options, will they share the cache?
A No. The cache key is calculated based on the URL, method, headers, and body. Requests with the same URL but different cache or next.revalidate settings will be treated as different cache entries.
Q How does Promise.all handle a failed request?
A Promise.all is an "all-or-nothing" operation—if any single request fails, the entire promise is rejected. If you need to handle errors, wrap each fetch call in Promise.allSettled or a try-catch block. A common pattern is const results = await Promise.all(urls.map(u => fetch(u).catch(() => null))).
Q How is the timeout for fetch handled in RSC?
A fetch does not have a built-in timeout. You can wrap it in an AbortController: const ctrl = new AbortController(); setTimeout(() => ctrl.abort(), 5000); fetch(url, { signal: ctrl.signal }). It is recommended to encapsulate a unified fetch client at the app level.
Q Can I use a third-party HTTP client (such as axios) in RSC?
A Yes, but you’ll lose the extended features of Next.js’s fetch, such as automatic caching, tags, and revalidation. If you use axios, you’ll need to implement caching logic manually or wrap a fetch-compatible layer around axios. We recommend using the native fetch method whenever possible.

📖 Summary


📝 Exercises

  1. Basic Exercise (⭐): Create a app/time-demo/page.tsx, use cache: 'no-store' and cache: 'force-cache' to make separate requests to the World Time API, compare the difference between the two timestamps, and verify the caching behavior.

  2. Advanced Problem (⭐⭐): Build a app/parallel-demo/page.tsx that uses Promise.all to fetch /users, /posts, and /comments (using the JSONPlaceholder API), and render each data point within a separate <Suspense> bounding box to demonstrate a streaming loading effect.

  3. Challenge (⭐⭐⭐): Create a task list page that supports CRUD operations: app/tasks/page.tsx (display the task list, using tags for caching) and app/tasks/actions.ts (call revalidateTag('tasks') to refresh the list after adding or deleting a task). Implement an optimistic update to ensure the list is refreshed immediately after a write operation.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏