nextjs-react
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNext.js & React Best Practices
Next.js & React 最佳实践
Provides production-grade patterns for React and Next.js applications using the App Router, covering rendering strategies, data fetching, state management, and performance optimization ordered by impact.
本文提供使用App Router构建React和Next.js应用的生产级模式,涵盖渲染策略、数据获取、状态管理以及按影响优先级排序的性能优化方案。
Rendering Strategy: Server First
渲染策略:优先使用服务器组件
Default to Server Components. Move to Client Components only when the component requires browser APIs, event listeners, or React hooks (, ).
useStateuseEffecttsx
// Server Component (default) - no 'use client' directive needed
async function ProductList() {
const products = await db.products.findMany() // direct DB access
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}
// Client Component - only when interactivity is required
'use client'
function AddToCartButton({ productId }: { productId: string }) {
const [added, setAdded] = useState(false)
return <button onClick={() => setAdded(true)}>{added ? 'Added' : 'Add'}</button>
}Composition pattern: push boundaries to leaf nodes. Wrap only the interactive slice, not the whole page.
'use client'tsx
// Correct: Server Component renders most of the tree
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id)
return (
<div>
<h1>{product.name}</h1> {/* stays on server */}
<p>{product.description}</p> {/* stays on server */}
<AddToCartButton productId={product.id} /> {/* only this is client */}
</div>
)
}默认使用Server Components。仅当组件需要浏览器API、事件监听器或React钩子(、)时,才改用Client Components。
useStateuseEffecttsx
// Server Component(默认)- 无需'use client'指令
async function ProductList() {
const products = await db.products.findMany() // 直接访问数据库
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}
// Client Component - 仅在需要交互时使用
'use client'
function AddToCartButton({ productId }: { productId: string }) {
const [added, setAdded] = useState(false)
return <button onClick={() => setAdded(true)}>{added ? 'Added' : 'Add'}</button>
}组合模式:将边界推至叶子节点。仅包裹需要交互的部分,而非整个页面。
'use client'tsx
// 正确:Server Component渲染大部分组件树
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id)
return (
<div>
<h1>{product.name}</h1> {/* 保留在服务器端 */}
<p>{product.description}</p> {/* 保留在服务器端 */}
<AddToCartButton productId={product.id} /> {/* 仅该部分是客户端组件 */}
</div>
)
}Data Fetching: Eliminate Waterfalls (CRITICAL)
数据获取:消除请求瀑布(至关重要)
Sequential awaits that do not depend on each other create avoidable latency. Run independent fetches in parallel.
tsx
// Bad: 600ms sequential wait
const user = await getUser(id)
const posts = await getPosts(id) // waits for user unnecessarily
const stats = await getStats(id) // waits for posts unnecessarily
// Good: ~200ms parallel fetch
const [user, posts, stats] = await Promise.all([
getUser(id),
getPosts(id),
getStats(id),
])Avoid blocking fetches before a branch that exits early:
tsx
// Bad: fetches userData even when skipping
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId)
if (skipProcessing) return { skipped: true }
return processUserData(userData)
}
// Good: fetch only when needed
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) return { skipped: true }
const userData = await fetchUserData(userId)
return processUserData(userData)
}互不依赖的顺序会造成不必要的延迟。并行执行独立的请求。
awaittsx
// 不佳:顺序等待600ms
const user = await getUser(id)
const posts = await getPosts(id) // 不必要地等待user请求完成
const stats = await getStats(id) // 不必要地等待posts请求完成
// 优化:并行请求约200ms完成
const [user, posts, stats] = await Promise.all([
getUser(id),
getPosts(id),
getStats(id),
])避免在提前退出的分支前执行阻塞性请求:
tsx
// 不佳:即使跳过处理仍会获取userData
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId)
if (skipProcessing) return { skipped: true }
return processUserData(userData)
}
// 优化:仅在需要时获取数据
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) return { skipped: true }
const userData = await fetchUserData(userId)
return processUserData(userData)
}Fetch data where it is needed
在需要数据的组件中获取数据
Next.js automatically deduplicates calls with the same URL within a single request. Fetch in the component that needs the data; do not thread props down.
fetchtsx
// Both components call getUser — only one network request is made
async function Header() {
const user = await getUser()
return <nav>{user.name}</nav>
}
async function ProfilePage() {
const user = await getUser() // deduplicated automatically
return <Header />, <main>{user.bio}</main>
}Use for non- data sources (ORM calls, SDK calls):
React.cachefetchts
import { cache } from 'react'
import 'server-only'
export const getUser = cache(async (id: string) => {
return db.users.findUnique({ where: { id } })
})Next.js会自动对同一请求内相同URL的调用进行去重。在需要数据的组件中直接获取,无需通过props层层传递。
fetchtsx
// 两个组件都调用getUser — 仅会发起一次网络请求
async function Header() {
const user = await getUser()
return <nav>{user.name}</nav>
}
async function ProfilePage() {
const user = await getUser() // 自动去重
return <Header />, <main>{user.bio}</main>
}对于非的数据源(ORM调用、SDK调用),使用:
fetchReact.cachets
import { cache } from 'react'
import 'server-only'
export const getUser = cache(async (id: string) => {
return db.users.findUnique({ where: { id } })
})Streaming with Suspense
使用Suspense实现流式渲染
Wrap slow data-dependent subtrees in to unblock the rest of the page:
<Suspense>tsx
export default async function Dashboard() {
return (
<>
<StaticHeader />
<Suspense fallback={<Skeleton />}>
<SlowAnalyticsWidget /> {/* streams in when ready */}
</Suspense>
<Suspense fallback={<Skeleton />}>
<SlowActivityFeed /> {/* independent stream */}
</Suspense>
</>
)
}Use a route file at the route segment level for page-wide skeletons.
loading.tsx将依赖慢数据的子树用包裹,避免阻塞页面其余部分的渲染:
<Suspense>tsx
export default async function Dashboard() {
return (
<>
<StaticHeader />
<Suspense fallback={<Skeleton />}>
<SlowAnalyticsWidget /> {/* 数据就绪后流式渲染 */}
</Suspense>
<Suspense fallback={<Skeleton />}>
<SlowActivityFeed /> {/* 独立流式渲染 */}
</Suspense>
</>
)
}可在路由分段级别使用路由文件,实现页面级骨架屏。
loading.tsxPreload pattern
预加载模式
Kickstart a fetch before an async check completes:
tsx
import Item, { preload } from '@/components/Item'
export default async function Page({ params }: { params: { id: string } }) {
preload(params.id) // starts immediately
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={params.id} /> : null
}在异步检查完成前提前启动请求:
tsx
import Item, { preload } from '@/components/Item'
export default async function Page({ params }: { params: { id: string } }) {
preload(params.id) // 立即启动预加载
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={params.id} /> : null
}State Management: Right Tool for the Scope
状态管理:根据场景选择合适工具
Choose state tooling based on scope—over-engineering global state is a common source of bundle bloat.
| Scenario | Recommended tool |
|---|---|
| Local component state | |
| Shared UI state (theme, modals) | |
| Complex client state sharing | Zustand or Jotai |
| Server data + caching on client | TanStack Query |
| Form state | React Hook Form |
Lazy-initialize expensive state to avoid per-render recalculation:
tsx
// Bad: JSON.parse runs on every render
const [config, setConfig] = useState(JSON.parse(localStorage.getItem('config') ?? '{}'))
// Good: callback runs once at mount
const [config, setConfig] = useState(() => JSON.parse(localStorage.getItem('config') ?? '{}'))根据作用范围选择状态管理工具——过度设计全局状态是导致包体积膨胀的常见原因。
| 场景 | 推荐工具 |
|---|---|
| 组件本地状态 | |
| 共享UI状态(主题、模态框) | |
| 复杂客户端状态共享 | Zustand或Jotai |
| 服务器数据+客户端缓存 | TanStack Query |
| 表单状态 | React Hook Form |
使用惰性初始化优化昂贵状态,避免每次渲染都重复计算:
tsx
// 不佳:JSON.parse在每次渲染时都执行
const [config, setConfig] = useState(JSON.parse(localStorage.getItem('config') ?? '{}'))
// 优化:回调仅在组件挂载时执行一次
const [config, setConfig] = useState(() => JSON.parse(localStorage.getItem('config') ?? '{}'))Bundle Size: Reduce JavaScript Sent to the Client
包体积优化:减少发送到客户端的JavaScript
Large bundles are a persistent tax on every user session. Audit with or .
next build --debug@next/bundle-analyzer大包体积会持续影响用户会话性能。可使用或进行审计。
next build --debug@next/bundle-analyzerDynamic imports for non-critical code
对非关键代码使用动态导入
tsx
import dynamic from 'next/dynamic'
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <Skeleton />,
ssr: false, // client-only libraries (e.g., chart.js)
})tsx
import dynamic from 'next/dynamic'
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <Skeleton />,
ssr: false, // 仅客户端使用的库(如chart.js)
})Avoid barrel file re-exports for large libraries
避免对大型库使用桶文件重导出
tsx
// Bad: imports entire lodash
import { debounce } from 'lodash'
// Good: imports only debounce
import debounce from 'lodash/debounce'tsx
// 不佳:导入整个lodash库
import { debounce } from 'lodash'
// 优化:仅导入debounce
import debounce from 'lodash/debounce'Mark server-only modules
标记仅服务器可用的模块
ts
import 'server-only' // throws at build time if imported from a Client Componentts
import 'server-only' // 如果被Client Component导入,构建时会抛出错误Re-render Optimization
重渲染优化
Address re-renders after eliminating waterfalls and reducing bundle size; micro-optimizations rarely move real-world metrics.
- Use to skip re-rendering a component whose props have not changed.
memo - Use for expensive pure calculations that depend on props/state.
useMemo - Use to stabilize function references passed to memoized children.
useCallback - Combine multiple loop passes over the same array into a single .
reduce
tsx
// Bad: scans messages 8 separate times
const unread = messages.filter(m => !m.read)
const pinned = messages.filter(m => m.pinned)
const recent = messages.filter(m => isRecent(m))
// Good: single pass
const { unread, pinned, recent } = messages.reduce(
(acc, m) => {
if (!m.read) acc.unread.push(m)
if (m.pinned) acc.pinned.push(m)
if (isRecent(m)) acc.recent.push(m)
return acc
},
{ unread: [], pinned: [], recent: [] }
)在解决请求瀑布和包体积问题后再处理重渲染;微优化对实际指标影响极小。
- 使用跳过props未变化的组件重渲染。
memo - 使用缓存依赖props/state的昂贵纯计算结果。
useMemo - 使用稳定传递给已记忆子组件的函数引用。
useCallback - 将对同一数组的多次循环遍历合并为单次操作。
reduce
tsx
// 不佳:对messages进行8次独立扫描
const unread = messages.filter(m => !m.read)
const pinned = messages.filter(m => m.pinned)
const recent = messages.filter(m => isRecent(m))
// 优化:单次遍历完成所有操作
const { unread, pinned, recent } = messages.reduce(
(acc, m) => {
if (!m.read) acc.unread.push(m)
if (m.pinned) acc.pinned.push(m)
if (isRecent(m)) acc.recent.push(m)
return acc
},
{ unread: [], pinned: [], recent: [] }
)Server Actions for Mutations
使用Server Actions处理数据变更
Use Server Actions for form submissions and data mutations. They run on the server with direct data-layer access and no extra API route needed.
tsx
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
await db.posts.create({ data: { title } })
revalidatePath('/posts')
}
// app/posts/new/page.tsx
import { createPost } from '../actions'
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" required />
<button type="submit">Create</button>
</form>
)
}Call or after mutations to invalidate cached data.
revalidatePathrevalidateTag使用Server Actions处理表单提交和数据变更。它们在服务器端运行,可直接访问数据层,无需额外API路由。
tsx
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
await db.posts.create({ data: { title } })
revalidatePath('/posts')
}
// app/posts/new/page.tsx
import { createPost } from '../actions'
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" required />
<button type="submit">创建</button>
</form>
)
}数据变更后调用或,使缓存数据失效。
revalidatePathrevalidateTagSecurity: Protect Sensitive Data
安全:保护敏感数据
Never pass sensitive objects or values as props to Client Components. Use React taint APIs to enforce this at development time:
ts
// next.config.js
module.exports = { experimental: { taint: true } }
// utils/user.ts
import { experimental_taintObjectReference, experimental_taintUniqueValue } from 'react'
export async function getUser(id: string) {
const user = await db.users.findUnique({ where: { id } })
experimental_taintObjectReference('Do not pass full user to client', user)
experimental_taintUniqueValue('Do not pass token to client', user, user.apiToken)
return user
}切勿将敏感对象或值作为props传递给Client Components。使用React污染API在开发阶段强制实施此规则:
ts
// next.config.js
module.exports = { experimental: { taint: true } }
// utils/user.ts
import { experimental_taintObjectReference, experimental_taintUniqueValue } from 'react'
export async function getUser(id: string) {
const user = await db.users.findUnique({ where: { id } })
experimental_taintObjectReference('请勿将完整用户对象传递给客户端', user)
experimental_taintUniqueValue('请勿将令牌传递给客户端', user, user.apiToken)
return user
}Accessibility
可访问性
- Use semantic HTML (,
<button>,<nav>,<main>) over<article>with roles.<div> - Add /
aria-labelon interactive elements that lack visible text.aria-describedby - Ensure keyboard navigability: focusable elements in logical DOM order.
- Provide text on all
altcomponents (set<Image>for decorative images).alt="" - Test with or browser extensions during development.
axe-core
- 使用语义化HTML(、
<button>、<nav>、<main>)替代带role属性的<article>。<div> - 对缺乏可见文本的交互元素添加/
aria-label属性。aria-describedby - 确保键盘可导航:可聚焦元素按逻辑DOM顺序排列。
- 为所有组件提供
<Image>文本(装饰性图片设置alt)。alt="" - 开发期间使用或浏览器扩展进行测试。
axe-core
File and Project Organization
文件与项目组织结构
app/
(marketing)/ # route group — no URL segment
page.tsx
dashboard/
layout.tsx
page.tsx
_components/ # co-located, not a route
Sidebar.tsx
components/ # shared UI
ui/
Button.tsx
lib/ # utilities, data-fetching helpers
db.ts
auth.ts- Co-locate components with the routes that use them under .
_components/ - Share truly reusable components in .
components/ - Keep data-fetching helpers in and mark with
lib/where appropriate.server-only
app/
(marketing)/ # 路由组 — 不会生成URL分段
page.tsx
dashboard/
layout.tsx
page.tsx
_components/ # 组件与路由共存,不属于路由
Sidebar.tsx
components/ # 共享UI组件
ui/
Button.tsx
lib/ # 工具函数、数据获取助手
db.ts
auth.ts- 将组件与使用它们的路由放在同一目录下的中。
_components/ - 在中存放真正可复用的组件。
components/ - 数据获取助手放在中,并根据情况标记为
lib/。server-only
Quick Reference
快速参考
| Rule | Impact |
|---|---|
Parallelize independent | CRITICAL |
| Default to Server Components | HIGH |
Keep | HIGH |
| Dynamic-import non-critical modules | HIGH |
Stream slow widgets with | HIGH |
Deduplicate fetches with | MEDIUM |
Lazy-initialize | MEDIUM |
| Combine loop iterations | MEDIUM |
| Memoize expensive calculations | LOW |
| 规则 | 影响级别 |
|---|---|
并行执行独立的 | 至关重要 |
| 默认使用Server Components | 高 |
将 | 高 |
| 动态导入非关键模块 | 高 |
使用 | 高 |
使用 | 中 |
使用回调函数惰性初始化 | 中 |
| 合并循环遍历操作 | 中 |
| 记忆化昂贵计算结果 | 低 |
Additional Resources
额外资源
- — Complete data fetching patterns: caching, revalidation, tags, Server Actions, and taint APIs
references/data-fetching.md - — Performance optimization: bundle analysis, image optimization, font loading, Core Web Vitals
references/performance.md - — State management decision guide: hook patterns, Zustand, Jotai, TanStack Query integration
references/state-management.md
- — 完整的数据获取模式:缓存、重新验证、标签、Server Actions和污染API
references/data-fetching.md - — 性能优化:包分析、图片优化、字体加载、核心Web指标
references/performance.md - — 状态管理决策指南:钩子模式、Zustand、Jotai、TanStack Query集成 ",
references/state-management.md