本文总结:
- 服务端组件 → 直接 fetch或 ORM 查询。
- 客户端组件 → 用 useHook 或 swr/react-query。
- 缓存 → 四种机制:请求记忆、数据缓存、完整路由缓存、客户端路由缓存。
- 流式渲染 → 用 Suspense或loading.js提高性能。
- Server Actions → 在服务端运行函数,配合表单或事件调用,同时能更新数据并触发缓存重新验证。
数据获取
服务端组件
官方文档data fetch:nextjs.org/docs/app/ge...
在服务端组件中,有两种方式,直接使用fetch和使用orm查询
第一种方式:直接在异步函数中获取数据:
            
            
              javascript
              
              
            
          
          export default async function Page() {
  const data = await fetch('<https://api.vercel.app/blog>')
  const posts = await data.json()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}第二种方式:直接使用orm框架查询数据,不用调用第三方接口:
            
            
              javascript
              
              
            
          
          import { db, posts } from '@/lib/db'
 
export default async function Page() {
  const allPosts = await db.select().from(posts)
  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}客户端组件
在客户端组件中,有两种推荐的方式:使用use Hook或者使用第三方库如swr和react-query
第一种方式:使用React的use Hook,可以使用 use 和流式数据获取,从服务端获取数据,然后传递到客户端,服务端代码如下:
            
            
              javascript
              
              
            
          
          import Posts from '@/app/ui/posts'
import { Suspense } from 'react'
 
export default function Page() {
  // Don't await the data fetching function
  const posts = getPosts()
 
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Posts posts={posts} />
    </Suspense>
  )
}客户端使用use去读Promise
            
            
              typescript
              
              
            
          
          'use client'
import { use } from 'react'
 
export default function Posts({
  posts,
}: {
  posts: Promise<{ id: string; title: string }[]>
}) {
  const allPosts = use(posts)
 
  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}第二种方式:使用第三方的请求库,比较好用的是swr和react-query,swr是vercel自己的,更推荐
            
            
              javascript
              
              
            
          
          'use client'
import useSWR from 'swr'
 
const fetcher = (url) => fetch(url).then((r) => r.json())
 
export default function BlogPage() {
  const { data, error, isLoading } = useSWR(
    '<https://api.vercel.app/blog>',
    fetcher
  )
 
  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>
 
  return (
    <ul>
      {data.map((post: { id: string; title: string }) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}请求缓存
请求缓存就是删除重复的fetch请求,只需要调用一次,其他的拿缓存结果使用就行。
一种方法是请求记忆,在单个渲染过程中,fetch使用GET或 且具有相同 URL 和选项的调用将被合并为一个请求。还可以fetch使用 Next.js 的数据缓存来删除重复请求,例如通过cache: 'force-cache'进行设置fetch。
另一种方法不使用fetch,比如使用ORM的场景,可以用 React的 的cache 来包裹函数
            
            
              typescript
              
              
            
          
          import { cache } from 'react'
import { db, posts, eq } from '@/lib/db'
 
export const getPost = cache(async (id: string) => {
  const post = await db.query.posts.findFirst({
    where: eq(posts.id, parseInt(id)),
  })
})流式请求
当在服务端组件使用 async/await ,Next.js会启用 dynamic rendering.,每次用户请求时,都会在服务器上获取并渲染数据。如果有任何缓慢的数据请求,整个路由将被阻止渲染。为了解决这个问题,可以把HTML分成小的chunks,有两种方式实现,一个是用loading.js包裹,另一个使用Suspense组件包裹。
            
            
              javascript
              
              
            
          
          import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'
 
export default function BlogPage() {
  return (
    <div>
      {/* This content will be sent to the client immediately */}
      <header>
        <h1>Welcome to the Blog</h1>
        <p>Read the latest posts below.</p>
      </header>
      <main>
        {/* Any content wrapped in a <Suspense> boundary will be streamed */}
        <Suspense fallback={<BlogListSkeleton />}>
          <BlogList />
        </Suspense>
      </main>
    </div>
  )
}Next.js四种缓存
| 机制 | 缓存什么 | 在哪里 | 目的 | 期间 | 
|---|---|---|---|---|
| 请求记忆 | 函数的返回值 | 服务器 | 在 React 组件树中重用数据 | 每个请求的生命周期 | 
| 数据缓存 | 数据 | 服务器 | 跨用户请求和部署存储数据 | 持久性(可重新验证) | 
| 完整路由缓存 | HTML 和 RSC payload | 服务器 | 降低渲染成本并提高性能 | 持久性(可重新验证) | 
| 客户端路由缓存 | RSC payload | 客户 | 减少导航时的服务器请求 | 用户会话或基于时间 | 
请求记忆
Next.js 扩展了fetchAPI,可以自动记忆具有相同 URL 和选项的请求。这意味着你可以在 React 组件树的多个位置针对同一数据调用 fetch 函数,但只需执行一次。
例如,如果您需要跨路由使用相同的数据(例如在布局、页面和多个组件中),则无需在树的顶部获取数据,也无需在组件之间转发 props。相反,您可以在需要数据的组件中获取数据,而不必担心通过网络对同一数据进行多次请求所带来的性能影响。
            
            
              csharp
              
              
            
          
          async function getItem() {
  // The `fetch` function is automatically memoized and the result
  // is cached
  const res = await fetch('https://.../item/1')
  return res.json()
}
 
// This function is called twice, but only executed the first time
const item = await getItem() // cache MISS
 
// The second call could be anywhere in your route
const item = await getItem() // cache HIT数据缓存
Next.js 具有内置数据缓存,可持久保存传入服务器请求 和部署的 数据提取结果。这是因为 Next.js 扩展了原生fetchAPI,允许服务器上的每个请求设置自己的持久缓存语义。
可以使用cache和next.revalidate选项fetch来配置缓存行为。
数据缓存和请求记忆的区别: 虽然两种缓存机制都可以通过重复使用缓存数据来提高性能,但数据缓存在传入的请求和部署中是持久的,而记忆仅持续请求的整个生命周期。
重新验证缓存,也就是缓存过期吧,有两种方式:
- 基于时间的重新验证:在设定的时间间隔后,数据会在下一次请求时重新验证。适用于变化不频繁、对实时性要求不高的场景。
- 按需重新验证:由事件触发的数据刷新,例如表单提交或 CMS 内容更新。可通过标签或路径的方式一次性刷新多组数据,确保页面尽快展示最新内容。
基于时间的重新验证:
可以使用next.revalidate选项fetch设置资源的缓存时间(以秒为单位)。
            
            
              php
              
              
            
          
          // Revalidate at most every hour
fetch('https://...', { next: { revalidate: 3600 } })注意,例如设置了60s,那么在60秒内的请求都返回缓存数据,超过60s的第一个请求还是返回缓存数据,,第二个请求才是新的数据
按需重新验证:
可以根据需要通过路径 ( revalidatePath) 或缓存标签 ( revalidateTag) 重新验证数据。
            
            
              scss
              
              
            
          
          import { revalidatePath, revalidateTag } from "next/cache";
revalidatePath(path); // 按路径刷新
revalidateTag(tag); // 按标签刷新如果不想缓存任何数据,可以使用:
            
            
              csharp
              
              
            
          
          let data = await fetch('<https://api.vercel.app/blog>', { cache: 'no-store' })完整路由缓存
Next.js 在构建的时候会自动渲染和缓存路由,这样当访问路由的时候,可以直接使用缓存中的路由而不用从零开始在服务端渲染,从而加快页面加载速度。需要了解以下几个原理:
- 服务端React渲染:在服务器上,Next.js 使用 React 的 API 来编排渲染。渲染工作被拆分成多个块:通过单独的路由和 Suspense 边界。第一步把React服务端组件渲染为RSC payload,第二步使用rsc payload和js渲染html,实现不用等待内容全渲染完,可以流式传输响应
- Next.js服务端缓存:完整路由缓存,缓存的是HTML和RSC payload
- 客户端 React水合:主要有三步:第一步立即显示HTML和服务端组件的非交互预览,第二步rsc payload更新客户端DOM,第三步js水合客户端组件,使之具有交互性
- Next.js客户端缓存:rsc payload 会缓存在客户端内存中,用于路由导航等内容
- 后续导航:先检查rsc payload在不在缓存,如果在就不重新发送请求
如何不使用缓存?
使用dynamic = 'force-dynamic'或revalidate = 0路由段配置选项
客户端路由缓存
Next.js 有一个内存客户端路由器缓存,用于存储路由段的 RSC 有效负载,按布局、加载状态和页面拆分。当用户在路由之间导航时,Next.js 会缓存已访问的路由段,并预取用户可能导航到的路由。这样可以实现即时的前进/后退导航,导航之间无需重新加载整个页面,并且在共享布局中保留浏览器状态和 React 状态。
Server Actions
Server Actions 是指在服务端执行的异步函数,它们可以在服务端和客户端组件中使用,来处理 Next.js 应用中的数据提交和更改。
定义一个 Server Action 需要使用 React 的 "use server" 指令。use server 可以放到 async函数的顶部或者放在一个单独文件的顶部,下面是在服务端组件和客户端组件中的用法
服务端组件
服务端组件:两种方式都可以使用,例如:
            
            
              javascript
              
              
            
          
          // actions.ts
export async function createPost(formData: FormData) {
  'use server'
  const title = formData.get('title')
  const content = formData.get('content')
 
  // Update data
  // Revalidate cache
}
 
export async function deletePost(formData: FormData) {
  'use server'
  const id = formData.get('id')
 
  // Update data
  // Revalidate cache
}
// page.tsx 直接在异步函数加 use server
export default function Page() {
  // Server Action
  async function createPost(formData: FormData) {
    'use server'
    // ...
  }
  return <></>
}客户端组件
客户端组件中使用的时候,需要先创建一个文件,在顶部添加 "use server" 指令:
            
            
              javascript
              
              
            
          
          // 单独的文件
'use server'
 
export async function createPost() {}
// 客户端组件
'use client'
import { createPost } from '@/app/actions'
 
export function Button() {
  return <button formAction={createPost}>Create</button>
}也可以作为props传递给客户端组件:
            
            
              javascript
              
              
            
          
          <ClientComponent updateItemAction={updateItem} />
'use client'
 
export default function ClientComponent({
  updateItemAction,
}: {
  updateItemAction: (formData: FormData) => void
}) {
  return <form action={updateItemAction}>{/* ... */}</form>
}调用方式
有两种可以调用 server function 的办法:
- 在Forms里调用,React扩展了HTML的 <form>,可以通过action属性调用,调用的时候会自动接受FormData对象
            
            
              javascript
              
              
            
          
          // form.tsx
import { createPost } from '@/app/actions'
 
export function Form() {
  return (
    <form action={createPost}>
      <input type="text" name="title" />
      <input type="text" name="content" />
      <button type="submit">Create</button>
    </form>
  )
}
//action.ts
'use server'
 
export async function createPost(formData: FormData) {
  const title = formData.get('title')
  const content = formData.get('content')
 
  // Update data
  // Revalidate cache
}- Event Handlers 事件处理,例如onClick函数
            
            
              javascript
              
              
            
          
          'use client'
 
import { incrementLike } from './actions'
import { useState } from 'react'
 
export default function LikeButton({ initialLikes }: { initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes)
 
  return (
    <>
      <p>Total Likes: {likes}</p>
      <button
        onClick={async () => {
          const updatedLikes = await incrementLike()
          setLikes(updatedLikes)
        }}
      >
        Like
      </button>
    </>
  )
}