React 与 Next.js 性能优化模式。适用于编写、审查或重构代码以确保最佳性能。涉及组件、数据获取、构建包优化、减少重渲染或服务端组件架构时触发。
反应性能模式是一项面向实际任务的技能,主要用于React and Next.js 应用程序的性能优化指南;Patterns. scross 7 类, 按效果排列;
使用时应结合输入条件选择合适的执行方式,核对必要参数、依赖环境与输出内容,并按原始要求处理异常情况。该技能适合需要稳定复用相关能力的场景,可作为自动化工作流的一部分,也便于后续检查、调整和扩展。从功能定位来看,该技能强调把分散的操作要求整理成清晰、可复用的处理流程,使用户能够围绕既定目标快速准备输入、选择执行方式并获得结构化结果。
实际使用前应先确认任务范围、数据来源、运行环境、必要权限和关键参数,再依据技能说明逐步执行;若输入条件不完整,应先补齐信息或采用保守配置,避免因错误假设导致结果偏离需求。执行过程中需要关注工具调用是否成功、接口或依赖是否可用、输出格式是否符合预期,并对异常提示、缺失字段和边界情况进行处理;涉及批量任务时,还应保存进度,避免中断后重复操作。
面向 React 和 Next.js 应用的性能优化指南。涵盖 7 类优化模式,按影响程度排序。详细示例见 references/ 目录。
| # | 类别 | 影响程度 |
|---|---|---|
| 1 | Async / Waterfalls | CRITICAL |
| 2 | Bundle Size | CRITICAL |
| 3 | Server Components | HIGH |
| 4 | Re-renders | MEDIUM |
| 5 | Rendering | MEDIUM |
| 6 | Client-Side Data | MEDIUM |
| 7 | JS Performance | LOW-MEDIUM |
npx clawhub@latest install react-performance
顺序 await 是 React 应用中最大的性能失误。
// BAD — 串行,需 3 次网络往返
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
// GOOD — 并行,仅需 1 次网络往返
const [user, posts, comments] = await Promise.all([
fetchUser(), fetchPosts(), fetchComments(),
])
将 await 移至实际使用该值的分支内。
// BAD — 两个分支均被阻塞
async function handle(userId: string, skip: boolean) {
const data = await fetchUserData(userId)
if (skip) return { skipped: true } // 即使跳过仍会等待
return process(data)
}
// GOOD — 仅在必要时阻塞
async function handle(userId: string, skip: boolean) {
if (skip) return { skipped: true } // 立即返回
return process(await fetchUserData(userId))
}
立即渲染布局,同时让依赖数据的区域独立加载。
// BAD — 整个页面被阻塞
async function Page() {
const data = await fetchData()
return
}
// GOOD — 布局立即渲染,数据流式加载
function Page() {
return (
}>
)
}
async function DataDisplay() {
const data = await fetchData()
return {data.content}
}
使用 use() 在组件间共享同一个 Promise,避免重复请求。
barrel 文件会加载数千个未使用的模块。直接导入可节省 200–800ms。
// BAD — 加载 1583 个模块
import { Check, X, Menu } from 'lucide-react'
// GOOD — 仅加载 3 个模块
import Check from 'lucide-react/dist/esm/icons/check'
import X from 'lucide-react/dist/esm/icons/x'
import Menu from 'lucide-react/dist/esm/icons/menu'
Next.js 13.5+:在配置中启用 experimental.optimizePackageImports。
常见受影响库:lucide-react、@mui/material、react-icons、@radix-ui、
lodash、date-fns。
import dynamic from 'next/dynamic'
const MonacoEditor = dynamic(
() => import('./monaco-editor').then((m) => m.MonacoEditor),
{ ssr: false }
)
分析、日志、错误追踪等 —— 使用 dynamic() 配合 { ssr: false } 在 hydration 后加载。
const preload = () => { void import('./monaco-editor') }
仅向客户端传递其实际使用的字段。
// BAD — 序列化全部 50 个用户字段
return
// GOOD — 仅序列化 1 个字段
return
RSC 在组件树内按顺序执行。需重构结构以支持并行。
// BAD — Sidebar 等待 header 请求完成
export default async function Page() {
const header = await fetchHeader()
return {header}
}
// GOOD — 同级异步组件同时发起请求
async function Header() { return {await fetchHeader()} }
async function Sidebar() { return }
export default function Page() { return }
import { cache } from 'react'
export const getCurrentUser = cache(async () => {
const session = await auth()
if (!session?.user?.id) return null
return await db.user.findUnique({ where: { id: session.user.id } })
})
请使用基础类型参数(而非内联对象)—— React.cache() 内部使用 Object.is 进行比对。
Next.js 会自动对 fetch 调用去重,但数据库查询、鉴权检查及计算类逻辑仍需显式使用 React.cache()。
import { after } from 'next/server'
export async function POST(request: Request) {
await updateDatabase(request)
after(async () => { logUserAction({ userAgent: request.headers.get('user-agent') }) })
return Response.json({ status: 'success' })
}
// BAD — 多余的状态 + effect
const [fullName, setFullName] = useState('')
useEffect(() => { setFullName(first + ' ' + last) }, [first, last])
// GOOD — 渲染时内联派生
const fullName = first + ' ' + last
// BAD — 每次 items 变更都会重新创建
const addItem = useCallback((item: Item) => {
setItems([...items, item])
}, [items])
// GOOD — 回调稳定,始终获取最新状态
const addItem = useCallback((item: Item) => {
setItems((curr) => [...curr, item])
}, [])
若仅在回调中读取状态,则无需订阅动态状态。
// BAD — 每次 searchParams 变更都触发重渲染
const searchParams = useSearchParams()
const handleShare = () => shareChat(chatId, { ref: searchParams.get('ref') })
// GOOD — 按需读取
const handleShare = () => {
const ref = new URLSearchParams(window.location.search).get('ref')
shareChat(chatId, { ref })
}
// BAD — 每次渲染都执行 JSON.parse
const [settings] = useState(JSON.parse(localStorage.getItem('s') || '{}'))
// GOOD — 仅执行一次
const [settings] = useState(() => JSON.parse(localStorage.getItem('s') || '{}'))
// BAD — 每像素变化都触发重渲染
const width = useWindowWidth(); const isMobile = width < 768
// GOOD — 仅当布尔值翻转时重渲染
const isMobile = useMediaQuery('(max-width: 767px)')
// BAD — 滚动时阻塞 UI
const handler = () => setScrollY(window.scrollY)
// GOOD — 非阻塞
const handler = () => startTransition(() => setScrollY(window.scrollY))
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const id = useMemo(() => computeAvatarId(user), [user])
return
})
function Profile({ user, loading }: Props) {
if (loading) return
return
}
注意:React Compiler 可自动处理,无需手动添加 memo()/useMemo()。
对于含 1000 条目的列表,浏览器可跳过约 990 条屏幕外条目(首屏渲染快约 10 倍)。
.list-item { content-visibility: auto; contain-intrinsic-size: 0 80px; }
避免重复创建,尤其适用于大型 SVG 节点。React Compiler 会自动执行此优化。
const skeleton =
function Container() { return {loading && skeleton} }
// BAD — 每个实例独立发起请求
useEffect(() => { fetch('/api/users').then(r => r.json()).then(setUsers) }, [])
// GOOD — 多个实例共享单次请求
const { data: users } = useSWR('/api/users', fetcher)
// BAD — O(n)
items.filter(i => allowed.includes(i.id))
// GOOD — O(1)
const allowedSet = new Set(allowed)
items.filter(i => allowedSet.has(i.id))
// BAD — 3 次遍历
const a = users.filter(u => u.isAdmin)
const t = users.filter(u => u.isTester)
// GOOD — 1 次遍历
const a: User[] = [], t: User[] = []
for (const u of users) { if (u.isAdmin) a.push(u); if (u.isTester) t.push(u) }
其他建议:尽早返回、在循环中缓存属性访问、将 RegExp 提升至循环外、热路径优先选用 for...of。