Next.js 16 Streaming Tutorial: Suspense + use cache 2026

May 12, 2026

Next.js 16 Streaming Tutorial: Suspense + use cache 2026

Streaming in Next.js 16 finally feels like one coherent story instead of three half-finished APIs. You wrap an async Server Component in <Suspense>, the server sends the shell over chunked transfer encoding, and a fast-streamed fallback hands off to real content as each query resolves1. What changed in 16.2 is the caching half of the picture: use cache, cacheLife, cacheTag, and updateTag are now stable, the unstable_ prefix is gone, and Turbopack is the default bundler for both dev and build2. This 2026 tutorial wires those primitives into a single runnable project so you can stream a slow page, cache only the parts that benefit from it, invalidate the cache on a Server Action, and watch read-your-writes work end-to-end.

TL;DR

You'll build a small "live posts" page in Next.js 16.2.6 that streams two independent server-rendered sections, falls back through loading.tsx for the route shell and <Suspense> for sibling cards, opts into the new caching model with cacheComponents: true, marks the slow data fetcher with 'use cache' plus a cacheLife('minutes') profile, and ends with a Server Action that calls updateTag so the next render shows the user's mutation immediately. Total build time: 25–35 minutes. No external services required; everything runs on localhost.

What you'll learn

  • How streaming actually works in Next.js 16 — what the server sends, what the browser does, and why Suspense is the boundary that matters
  • How to choose between loading.tsx and <Suspense> for route-level vs. component-level fallbacks
  • How to mark a slow Server Component as cacheable with the 'use cache' directive
  • How to control cache lifetime with cacheLife profiles and tag for invalidation with cacheTag
  • How to invalidate the right cache from a Server Action using updateTag vs. revalidateTag
  • How to migrate to async params and searchParams (a breaking change in Next.js 16)
  • How to verify the streaming works at the network layer with curl --no-buffer

Prerequisites

  • Node.js 22 LTS (Next.js 16 dropped Node 18; minimum is 20.9, but 22 LTS is the recommended baseline)3
  • pnpm 9+ or npm 10+
  • A terminal with curl available for the streaming verification step
  • Editor with TypeScript 5.7+ language server (TypeScript 6.0.3 is current; either will work4)
  • Basic familiarity with React Server Components — if any of this is new, read Mastering Next.js App Router patterns for scalable web apps first

Pinned dependencies for this tutorial (no latest tags — version drift is documented in this site's Next.js 16 migration tutorial):

PackageVersionWhy
next16.2.6Latest stable as of 2026-05-125
react19.2.6Required by Next.js 166
react-dom19.2.6Required by Next.js 16
@types/react19.2.14TS types matching React 19.2
typescript5.7.3Stable LTS line; 6.0.x also works

Step 1 — Scaffold the project

pnpm dlx create-next-app@16.2.6 stream-cache-demo \
  --typescript --app --no-tailwind --no-eslint --no-src-dir \
  --import-alias '@/*' --use-pnpm
cd stream-cache-demo

The flags pin the scaffold to Next.js 16.2.6, opt into the App Router, skip Tailwind/ESLint to keep the tutorial tight, and disable the src/ layout so the file paths in this post match what you see on disk.

Verify the install:

pnpm next --version
# Expected: 16.2.6
node --version
# Expected: v22.x.y (or v20.9.0+ if you're on the old LTS)

Step 2 — Opt into the new caching model

Next.js 16's caching is opt-in by default. Without the cacheComponents flag, all your dynamic Server Components run at request time and nothing is cached unless you explicitly mark it. Flip the switch in next.config.ts:

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  cacheComponents: true,
  // Custom profile for our streaming feed; sits alongside built-ins like 'seconds', 'minutes', and 'max'.
  cacheLife: {
    feed: { stale: 30, revalidate: 60, expire: 600 },
  },
};

export default nextConfig;

Two things to notice:

  1. cacheComponents: true is the renamed, stabilized successor to experimental.dynamicIO from Next.js 15. It enables Partial Prerendering-style behavior: a static shell streams immediately, dynamic sections stream as their data resolves7.
  2. The cacheLife config object lets you define custom named profiles. 'feed' here means: clients may serve the cached value for 30 seconds without checking the server, a background refresh starts after 60 seconds, and an unrequested entry is hard-evicted after 10 minutes. Built-in named profiles (seconds, minutes, hours, days, weeks, max) are always available alongside your custom ones8.

Step 3 — Fake a slow database

For a runnable tutorial you don't want to drag in Postgres, so simulate latency with a typed in-memory store. Create lib/db.ts:

// lib/db.ts
import 'server-only';

export type Post = {
  id: string;
  title: string;
  author: string;
  body: string;
  createdAt: string;
};

const store: Post[] = [
  {
    id: '1',
    title: 'Streaming the App Router',
    author: 'Ada',
    body: 'Server Components stream HTML over chunked transfer encoding...',
    createdAt: '2026-05-10T10:00:00.000Z',
  },
  {
    id: '2',
    title: 'Why use cache is opt-in',
    author: 'Grace',
    body: 'Next.js 16 flipped the default: dynamic at request time unless you opt in.',
    createdAt: '2026-05-11T09:30:00.000Z',
  },
];

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

export async function getPosts(): Promise<Post[]> {
  // Simulate a slow analytical query
  await sleep(1200);
  return store.slice().sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}

export async function getStats(): Promise<{ totalPosts: number; uniqueAuthors: number }> {
  // Simulate a fast aggregate
  await sleep(200);
  const uniqueAuthors = new Set(store.map((p) => p.author)).size;
  return { totalPosts: store.length, uniqueAuthors };
}

export async function createPost(input: Omit<Post, 'id' | 'createdAt'>): Promise<Post> {
  await sleep(150);
  const post: Post = {
    id: String(store.length + 1),
    ...input,
    createdAt: new Date().toISOString(),
  };
  store.unshift(post);
  return post;
}

The import 'server-only' line is a Next.js convention that throws at build time if a Client Component ever pulls this module — a cheap safety net for tutorials and a great habit in production code.

Step 4 — Render the page without caching, just streaming

This is the simplest streaming pattern: a single async Server Component with loading.tsx as the route-level fallback.

Create app/page.tsx:

// app/page.tsx
import { Suspense } from 'react';
import { getPosts, getStats } from '@/lib/db';

export const dynamic = 'force-dynamic'; // make sure we don't accidentally prerender

type PageProps = {
  searchParams: Promise<{ author?: string }>;
};

export default async function HomePage({ searchParams }: PageProps) {
  // Next.js 16: searchParams is now a Promise
  const { author } = await searchParams;

  return (
    <main style={{ maxWidth: 720, margin: '40px auto', fontFamily: 'system-ui' }}>
      <h1>Live posts {author ? `by ${author}` : ''}</h1>
      <Suspense fallback={<StatsSkeleton />}>
        <Stats />
      </Suspense>
      <Suspense fallback={<PostsSkeleton />}>
        <Posts authorFilter={author} />
      </Suspense>
    </main>
  );
}

async function Stats() {
  const { totalPosts, uniqueAuthors } = await getStats();
  return (
    <p style={{ color: '#666' }}>
      <strong>{totalPosts}</strong> posts from <strong>{uniqueAuthors}</strong> authors.
    </p>
  );
}

async function Posts({ authorFilter }: { authorFilter?: string }) {
  const posts = await getPosts();
  const filtered = authorFilter
    ? posts.filter((p) => p.author.toLowerCase() === authorFilter.toLowerCase())
    : posts;
  return (
    <ul style={{ listStyle: 'none', padding: 0 }}>
      {filtered.map((p) => (
        <li key={p.id} style={{ borderTop: '1px solid #eee', padding: '12px 0' }}>
          <h3 style={{ margin: 0 }}>{p.title}</h3>
          <small>
            by {p.author} on {new Date(p.createdAt).toLocaleString()}
          </small>
          <p>{p.body}</p>
        </li>
      ))}
    </ul>
  );
}

function StatsSkeleton() {
  return <p style={{ color: '#aaa' }}>Loading stats…</p>;
}

function PostsSkeleton() {
  return (
    <ul style={{ listStyle: 'none', padding: 0 }}>
      {[1, 2, 3].map((i) => (
        <li key={i} style={{ borderTop: '1px solid #eee', padding: '12px 0' }}>
          <div style={{ height: 16, width: '60%', background: '#eee' }} />
          <div style={{ height: 12, width: '30%', background: '#f3f3f3', margin: '6px 0' }} />
          <div style={{ height: 12, width: '90%', background: '#f3f3f3' }} />
        </li>
      ))}
    </ul>
  );
}

A few things worth pinning down:

  • Async params/searchParams. Next.js 16 made params and searchParams Promise-typed. Awaiting them is mandatory — using searchParams.author directly is a compile-time and runtime error9. If you have a Next.js 15 codebase, run npx @next/codemod@canary upgrade latest to automate the rewrite10.
  • Two sibling Suspense boundaries. Stats returns in ~200ms; Posts takes ~1200ms. With both wrapped, the page paints the heading immediately, swaps the stats fallback at ~200ms, and swaps the posts fallback at ~1200ms — no waterfall.
  • dynamic = 'force-dynamic'. Belt-and-braces for this step: we'll remove it once we add use cache so you can see the difference.

Optional route-level fallback. Create app/loading.tsx:

// app/loading.tsx
export default function Loading() {
  return (
    <main style={{ maxWidth: 720, margin: '40px auto', fontFamily: 'system-ui' }}>
      <h1 style={{ color: '#aaa' }}>Loading…</h1>
    </main>
  );
}

loading.tsx automatically wraps page.tsx (and any nested route segments below it) in a <Suspense> boundary that uses this file as its fallback11. It's the right tool when you want a single page-wide shell to stream immediately; the layout (your app/layout.tsx) stays interactive so the user can keep clicking the nav while the page swaps in.

Start the dev server:

pnpm next dev

Open http://localhost:3000. You should see the heading first, the stats appear after ~200ms, and the post list appear after ~1200ms. That's streaming working.

How do I stream server components in Next.js 16?

Wrap an async Server Component in <Suspense fallback={...}> (or place a loading.tsx next to the route's page.tsx), and Next.js will send the static shell plus your fallback over chunked transfer encoding immediately, then stream the async component's HTML as soon as its data resolves12. There is no separate streaming API to enable — every async Server Component automatically participates as long as a Suspense boundary exists somewhere above it.

To prove the chunks really arrive in stages, run this against your dev server:

curl --no-buffer -N http://localhost:3000 | head -c 4000

You'll see HTML print to the terminal in two visible pauses — the shell first, then the Stats chunk, then the Posts chunk. Each chunk is wrapped in <script>$RC(...)</script> calls that React uses to swap the streamed content into the right Suspense slot13.

When should I use loading.tsx vs Suspense in Next.js?

Use loading.tsx when the entire route is async and you want a single page-wide shell that streams immediately while keeping the layout interactive; use <Suspense> when one slow data dependency would otherwise block faster siblings and you want piece-by-piece streaming inside the page. They're not exclusive — loading.tsx provides the outer boundary, and <Suspense> provides the inner boundaries.

The practical heuristic:

PatternReach for
Whole route is slow; one fallback is fineloading.tsx
Page has a fast stat + a slow listSibling <Suspense> inside page.tsx
Layout reads runtime dataWrap the layout's runtime read in <Suspense> (loading.tsx won't cover layouts that themselves await runtime data)
Optimistic UI on a form<Suspense> + useTransition together

Step 5 — Cache the slow query with 'use cache'

Now the new part. Posts don't really need to be revalidated on every request — let's serve a 60-second-fresh cached copy that revalidates in the background. Modify lib/db.ts and add a cached variant of getPosts:

// lib/db.ts (additions)
import { cacheLife } from 'next/cache';
import { cacheTag } from 'next/cache';

export async function getCachedPosts(): Promise<Post[]> {
  'use cache';
  cacheLife('feed'); // matches the profile in next.config.ts
  cacheTag('posts'); // a label we can invalidate later
  return getPosts();
}

Three things happen here:

  1. 'use cache' marks the function's return value as cacheable. Next.js's compiler automatically generates a cache key from the function arguments — no keyParts argument required14.
  2. cacheLife('feed') applies the profile we defined in next.config.ts: 30s stale window, 60s background revalidate, 10-minute hard expire. You can also pass a built-in profile name ('minutes', 'hours', 'max') or an inline object like { stale: 60, revalidate: 300, expire: 3600 }15.
  3. cacheTag('posts') stamps a tag onto this cache entry. We'll use it from a Server Action in Step 7 to invalidate selectively.

Swap the call inside app/page.tsx:

// app/page.tsx (replace the Posts component)
import { getCachedPosts } from '@/lib/db';

async function Posts({ authorFilter }: { authorFilter?: string }) {
  const posts = await getCachedPosts();
  const filtered = authorFilter
    ? posts.filter((p) => p.author.toLowerCase() === authorFilter.toLowerCase())
    : posts;
  return (
    <ul style={{ listStyle: 'none', padding: 0 }}>
      {filtered.map((p) => (
        <li key={p.id} style={{ borderTop: '1px solid #eee', padding: '12px 0' }}>
          <h3 style={{ margin: 0 }}>{p.title}</h3>
          <small>
            by {p.author} on {new Date(p.createdAt).toLocaleString()}
          </small>
          <p>{p.body}</p>
        </li>
      ))}
    </ul>
  );
}

You can drop export const dynamic = 'force-dynamic' now — the 'use cache' directive is what controls whether this branch is cached, not the page-level flag. This is the move that defines the modern Next.js 16 streaming model: the static shell streams instantly, the cached posts section streams from the cache when it's warm, and only the uncached pieces pay the data-fetch cost on every request.

Reload http://localhost:3000 twice. The first load takes ~1200ms for the post list (the cache is empty). The second load returns the cached payload in single-digit milliseconds. Refresh again at the 30-second mark and you'll see the stale value served instantly while a background refresh happens.

What does the use cache directive do in Next.js 16?

The 'use cache' directive marks a function, component, or whole file as cacheable; Next.js generates a cache key from the function's arguments and any closure-captured values, stores the rendered output in the data and full-route caches, and serves the stored value (subject to the active cacheLife profile) on subsequent requests across the cluster16. It replaces both the implicit fetch cache and the older unstable_cache helper from the Next.js 15 era — and unlike those, it's compiler-aware, so you don't manually pass a keyParts array.

A short rules of thumb table:

ScopeWhere to put 'use cache'
Cache one function's return valueFirst line inside the function body
Cache an entire module's exportsFirst line at the top of the file
Cache a single Server ComponentFirst line inside the component body
Don't cache anything in this branchOmit the directive — request-time by default

Step 6 — Compose cacheTag so you can invalidate later

cacheTag is the verb; the tag is the noun. You can attach multiple tags to a single cache entry — useful when the same entry should be invalidated by either an author-level event ("Ada published a post") or a global event ("post catalog rebuilt"):

// lib/db.ts (variation)
export async function getCachedPostsByAuthor(author: string): Promise<Post[]> {
  'use cache';
  cacheLife('feed');
  cacheTag('posts');
  cacheTag(`posts:author:${author}`);
  const all = await getPosts();
  return all.filter((p) => p.author === author);
}

The same cache entry now responds to invalidation by 'posts' (everything) or 'posts:author:Ada' (just Ada's slice). The granularity is yours; the rule of thumb is one global tag plus one or two scoped tags per cached function.

Step 7 — Mutate, then invalidate with updateTag

Time for the part most streaming tutorials skip. A user creates a post via a Server Action; immediately after the action returns, they're going to navigate back to the home page expecting to see their new post. With revalidateTag, the cache invalidates with stale-while-revalidate semantics — meaning the user might still see the stale list on the next render. With updateTag, the cache is expired and refreshed within the same request, so the navigation lands on a fresh render. That's the read-your-writes guarantee17.

Create app/new/page.tsx:

// app/new/page.tsx
import { redirect } from 'next/navigation';
import { updateTag } from 'next/cache';
import { createPost } from '@/lib/db';

async function createPostAction(formData: FormData) {
  'use server';
  const title = String(formData.get('title') ?? '').trim();
  const author = String(formData.get('author') ?? '').trim();
  const body = String(formData.get('body') ?? '').trim();
  if (!title || !author || !body) {
    throw new Error('All fields are required');
  }
  await createPost({ title, author, body });
  // Read-your-writes: expire AND refresh within this request
  updateTag('posts');
  redirect('/');
}

export default function NewPostPage() {
  return (
    <main style={{ maxWidth: 720, margin: '40px auto', fontFamily: 'system-ui' }}>
      <h1>New post</h1>
      <form action={createPostAction} style={{ display: 'grid', gap: 12 }}>
        <label>
          Title <input name="title" required style={{ width: '100%' }} />
        </label>
        <label>
          Author <input name="author" required style={{ width: '100%' }} />
        </label>
        <label>
          Body <textarea name="body" required rows={5} style={{ width: '100%' }} />
        </label>
        <button type="submit">Publish</button>
      </form>
    </main>
  );
}

Submit the form. The action runs server-side, mutates the in-memory store, calls updateTag('posts') to expire the 'posts'-tagged entries, and redirects back to /. The home page's Posts component re-renders with the updated list immediately — no stale flash.

What is the difference between updateTag and revalidateTag in Next.js 16?

updateTag and revalidateTag both invalidate cache entries by tag, but updateTag does it with read-your-writes semantics (expire and refresh within the same request, served fresh on the next render) while revalidateTag uses stale-while-revalidate semantics (serve the stale value immediately, refresh in the background)18. Use updateTag after a user-initiated mutation when the user will navigate to a page that must show the change; use revalidateTag from webhooks or background jobs when slight delay is acceptable.

Two key constraints worth memorizing:

  • updateTag is Server-Action-only. It throws if called from a Route Handler, a Client Component, or middleware.
  • revalidateTag now takes a cacheLife profile as its second argument: revalidateTag('posts', 'max'). The recommended default is 'max' (longest-lived SWR), unless you need immediate hard expiration ({ expire: 0 } as the second arg)19.
Use caseReach for
User submits a form; will see the resultupdateTag inside the Server Action
Webhook fires from a CMSrevalidateTag('cms', 'max') in a route handler
Need the next visitor to wait for freshrevalidateTag('cms', { expire: 0 })
Cron job rebuilds a slow aggregaterevalidateTag('aggregate', 'max')

Step 8 — Verify streaming at the network layer

The browser DevTools network panel often displays the request as a single response — the chunks are real but the UI hides them. Use curl --no-buffer to confirm:

curl --no-buffer -N -s http://localhost:3000 \
  | tr -d '\n' \
  | tr '>' '>\n' \
  | grep -nE 'Loading stats|Loading posts|by Ada|by Grace' \
  | head -20

Run that command and watch the matches stream past the terminal. You should see "Loading stats" and "Loading posts" land first (the fallbacks), followed by lines referencing the streamed content. If the entire output prints at once with no visible delay, streaming is not working — most likely cause is a proxy or compression middleware buffering the response20.

You can also inspect the HTTP-level evidence:

curl -sI http://localhost:3000 | grep -i 'transfer-encoding\|content-encoding'
# Expected:
# Transfer-Encoding: chunked

chunked is the smoking gun — there's no Content-Length because the body length isn't known up front, and that's what allows progressive flushing.

Step 9 — A common gotcha: don't read runtime data in a layout that wraps cached pages

If your app/layout.tsx reads anything dynamic (cookies, headers, the request URL) and is not itself wrapped in a <Suspense> boundary, the entire route below it has to wait for that read before the shell can flush. The fix is to push the runtime read into its own client component, or to wrap a small server fragment in <Suspense> so the rest of the layout can ship immediately21.

Pattern:

// app/layout.tsx (corrected)
import { Suspense } from 'react';
import { CurrentUser } from './_components/current-user';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <nav>
          <a href="/">Home</a> · <a href="/new">New post</a> ·
          <Suspense fallback={<span style={{ color: '#999' }}></span>}>
            <CurrentUser />
          </Suspense>
        </nav>
        {children}
      </body>
    </html>
  );
}

CurrentUser can now await cookies() without blocking the rest of the page from streaming.

Verification

Run through the full checklist:

# 1. Confirm dev server boots on Next.js 16.2.6
pnpm next --version
# Expected: 16.2.6

# 2. Confirm chunked transfer encoding is on
curl -sI http://localhost:3000 | grep -i transfer-encoding
# Expected: Transfer-Encoding: chunked

# 3. Confirm streaming actually streams (you'll see two visible pauses)
time curl --no-buffer -N -s http://localhost:3000 > /dev/null
# Expected: real time between 1.2s and 1.5s

# 4. Confirm caching kicks in on the second load (sub-200ms)
time curl -s http://localhost:3000 > /dev/null
# Expected: real time under 200ms

# 5. Submit a new post and verify it appears immediately
curl -s -X POST -F 'title=Test' -F 'author=Tutorial' -F 'body=Hello' \
  http://localhost:3000/new -i | head -3
# Expected: HTTP/1.1 303 See Other with Location: /

If all five pass, you've shipped a streaming + caching pipeline that's correct from the request boundary all the way through to read-your-writes invalidation.

Troubleshooting

Type error: Property 'author' does not exist on type 'Promise<...>'. You forgot to await params or searchParams. In Next.js 16 they're Promises; the codemod (npx @next/codemod@canary upgrade latest) fixes most call sites automatically10.

use cache is not allowed inside a synchronous function. The function containing 'use cache' must be async. The directive applies to the return value of an async function, not to a sync helper. Convert the function signature to async and the error goes away.

cacheTag was called outside of a "use cache" scope. Both cacheTag and cacheLife must be invoked inside a function (or file) whose first statement is the 'use cache' directive. If you see this error, move the call below the directive, or hoist the entire logic into a 'use cache' function.

updateTag is only available inside Server Actions. You called updateTag from a Route Handler, a Client Component, or somewhere outside a 'use server' boundary. Move the call into a Server Action; if you genuinely need to invalidate from a webhook, use revalidateTag(tag, 'max') instead.

Streaming "works" in dev but not in production. Check your reverse proxy. A misconfigured Nginx (proxy_buffering on, the default) will buffer the entire response and break chunked streaming end-to-end. Add proxy_buffering off for the Next.js upstream, or test directly against node/pnpm next start to confirm the framework is shipping chunks before blaming Next.js22.

Next steps

You now have streaming, opt-in caching, and read-your-writes invalidation in a single page. From here:

  • Wire a real database — for Postgres, start with Production Postgres pooling with PgBouncer & Supabase Supavisor
  • Add per-row Server Actions that call updateTag(posts:author:${author}) for granular invalidation
  • Combine useTransition with <Suspense> for non-blocking optimistic UI on the new post form23
  • Move the in-memory store behind a Redis instance and re-use the same 'use cache' API — the directive doesn't care about the underlying datasource, only the function's arguments

If you're still migrating off the Pages Router, the Next.js Pages Router to App Router migration tutorial covers async params, codemods, and the layout traps that bite during the move. For the wider RSC mental model — what Server Components actually compile down to and why they ship zero JS — see React Server Components: the future of seamless rendering.


Footnotes

  1. Next.js, "Guides: Streaming." https://nextjs.org/docs/app/guides/streaming

  2. Next.js, "Next.js 16" (release blog post, 2025-10-22). https://nextjs.org/blog/next-16

  3. Next.js, "Upgrading: Version 16." https://nextjs.org/docs/app/guides/upgrading/version-16

  4. TypeScript releases on npm — npm view typescript version returned 6.0.3 (published 2026-04-16) at the time of writing; 5.7.x is still supported. https://www.npmjs.com/package/typescript

  5. npm view next time --json16.2.6 published 2026-05-07T19:01:54.751Z. https://www.npmjs.com/package/next

  6. Next.js, "Minimum React Version." https://nextjs.org/docs/messages/react-version

  7. Next.js, "next.config.js: cacheComponents." https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents

  8. Next.js, "Functions: cacheLife." https://nextjs.org/docs/app/api-reference/functions/cacheLife

  9. Next.js, "Dynamic APIs are Asynchronous." https://nextjs.org/docs/messages/sync-dynamic-apis

  10. Next.js, "Upgrading: Version 16 — Automated upgrade." https://nextjs.org/docs/app/guides/upgrading/version-16 2

  11. Next.js, "File-system conventions: loading.js." https://nextjs.org/docs/app/api-reference/file-conventions/loading

  12. Next.js, "App Router: Streaming." https://nextjs.org/learn/dashboard-app/streaming

  13. React, " — Reference." https://react.dev/reference/react/Suspense

  14. Next.js, "Directives: use cache." https://nextjs.org/docs/app/api-reference/directives/use-cache

  15. Next.js, "Functions: cacheLife — Default profiles." https://nextjs.org/docs/app/api-reference/functions/cacheLife

  16. Next.js 16.2 blog post — "use cache is now stable." https://nextjs.org/blog/next-16-2

  17. Next.js, "Functions: updateTag." https://nextjs.org/docs/app/api-reference/functions/updateTag

  18. Next.js, "Functions: revalidateTag." https://nextjs.org/docs/app/api-reference/functions/revalidateTag

  19. Next.js, "Getting Started: Revalidating." https://nextjs.org/docs/app/getting-started/revalidating

  20. Next.js, "Guides: Streaming — Proxy buffering caveats." https://nextjs.org/docs/app/guides/streaming

  21. Next.js, "Getting Started: Server and Client Components." https://nextjs.org/docs/app/getting-started/server-and-client-components

  22. Next.js GitHub Discussion #58865 — "Suspense And Loading" (proxy/buffering thread). https://github.com/vercel/next.js/discussions/58865

  23. React, "useTransition — Reference." https://react.dev/reference/react/useTransition


FREE WEEKLY NEWSLETTER

Stay on the Nerd Track

One email per week — courses, deep dives, tools, and AI experiments.

No spam. Unsubscribe anytime.