Langfuse 前端性能工程实践:基于 Vercel React Best Practices 的 40+ 条规则详解
Langfuse 前端性能工程实践:基于 Vercel React Best Practices 的 40+ 条规则详解
本文以 langfuse 仓库内置的 web/.agents/skills/vercel-react-best-practices/AGENTS.md 为骨架,系统讲解一套面向 React 与 Next.js 应用的性能优化规则体系。该文档由 Vercel Engineering 编写(Version 1.0.0),专为 AI Agent 与 LLM 在维护、生成、重构 React/Next.js 代码库时提供自动化一致性指导,同时适用于人工阅读。读完本文,你将掌握 8 大类别、40+ 条按影响优先级排序的规则,理解每条规则的错误/正确写法对比与影响指标,并能结合 langfuse 前端实际代码(Next.js 16.3.3 + React 19.2.4)落地这些模式。
规则体系概览:优先级驱动的重构清单
这套指南的核心价值在于"按影响排序",让自动化工具与工程师都能优先处理收益最大的问题。完整规则索引见 AGENTS.md,配套的规则速查文件为同目录下的 SKILL.md,每条规则的独立详解存放在 rules/ 目录(如 rules/async-parallel.md、rules/bundle-barrel-imports.md),每个规则文件均包含:为什么重要的简要说明、错误代码示例及解释、正确代码示例及解释、附加上下文与参考。
8 大类别按优先级划分如下:
| 优先级 | 类别 | 影响级别 | 规则前缀 |
|---|---|---|---|
| 1 | 消除瀑布流(Eliminating Waterfalls) | CRITICAL | async- |
| 2 | 包体积优化(Bundle Size) | CRITICAL | bundle- |
| 3 | 服务端性能(Server-Side) | HIGH | server- |
| 4 | 客户端数据获取(Client-Side) | MEDIUM-HIGH | client- |
| 5 | 重渲染优化(Re-render) | MEDIUM | rerender- |
| 6 | 渲染性能(Rendering) | MEDIUM | rendering- |
| 7 | JavaScript 性能 | LOW-MEDIUM | js- |
| 8 | 高级模式(Advanced) | LOW | advanced- |
整体思路是:先消除最致命的串行等待与包体积问题,再优化服务端渲染与数据获取,最后才是微观层面的重渲染与 JS 热路径优化。langfuse 前端恰好是这套体系的理想验证对象——web/package.json 显示其技术栈为 next: 16.3.3、react: 19.2.4,并使用 @tanstack/react-query(^5.85.1)与 superjson(2.2.2)处理数据获取与序列化。
1. 消除瀑布流(Eliminating Waterfalls):CRITICAL
瀑布流是性能的头号杀手。每一次串行的 await 都会叠加一整次网络延迟,消除瀑布流能带来最大的性能收益。
1.1 延迟 await 到真正需要时(Defer Await Until Needed)
把 await 移入实际使用它的分支,避免阻塞不需要该数据的代码路径。当被跳过的分支是高频路径、或被延迟的操作本身很昂贵时,这一优化价值尤其大。
错误写法会阻塞两个分支:
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId)
if (skipProcessing) {
// 立即返回,但仍然白白等待了 userData
return { skipped: true }
}
return processUserData(userData) // 只有这个分支用到 userData
}
正确写法只在实际需要时阻塞:
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) {
return { skipped: true } // 不等待,立即返回
}
const userData = await fetchUserData(userId) // 只在需要时获取
return processUserData(userData)
}
另一个经典场景是"尽早返回优化":先校验资源是否存在,再拉取权限,避免每次都拉权限:
// 错误:总是先拉取权限
async function updateResource(resourceId: string, userId: string) {
const permissions = await fetchPermissions(userId)
const resource = await getResource(resourceId)
if (!resource) return { error: 'Not found' }
if (!permissions.canEdit) return { error: 'Forbidden' }
return await updateResourceData(resource, permissions)
}
// 正确:按需获取
async function updateResource(resourceId: string, userId: string) {
const resource = await getResource(resourceId)
if (!resource) return { error: 'Not found' }
const permissions = await fetchPermissions(userId)
if (!permissions.canEdit) return { error: 'Forbidden' }
return await updateResourceData(resource, permissions)
}
1.2 基于依赖关系的并行化(Dependency-Based Parallelization)
影响级别:CRITICAL(2-10 倍提升)。对于存在部分依赖关系的操作,文档推荐使用 better-all 库自动让每个任务在最早期开始:
import { all } from 'better-all'
const { user, config, profile } = await all({
async user() { return fetchUser() },
async config() { return fetchConfig() },
async profile() {
return fetchProfile((await this.$.user).id) // 自动等待 user,但与其他任务并行
}
})
如果不引入额外依赖,也可以手写等价模式——先创建所有 Promise,最后统一 Promise.all():
const userPromise = fetchUser()
const profilePromise = userPromise.then(user => fetchProfile(user.id))
const [user, config, profile] = await Promise.all([
userPromise,
fetchConfig(),
profilePromise
])
1.3 防止 API 路由中的瀑布链(Prevent Waterfall Chains in API Routes)
影响级别:CRITICAL(2-10 倍提升)。在 API 路由与 Server Actions 中,即使暂时不需要 await,也要立即启动独立的操作。
// 错误:config 等 auth,data 等两者
export async function GET(request: Request) {
const session = await auth()
const config = await fetchConfig()
const data = await fetchData(session.user.id)
return Response.json({ data, config })
}
// 正确:auth 与 config 立即启动
export async function GET(request: Request) {
const sessionPromise = auth()
const configPromise = fetchConfig()
const session = await sessionPromise
const [config, data] = await Promise.all([
configPromise,
fetchData(session.user.id)
])
return Response.json({ data, config })
}
1.4 独立操作用 Promise.all()(Promise.all() for Independent Operations)
影响级别:CRITICAL(2-10 倍提升)。三个独立请求串行是 3 次往返,并行则只需 1 次:
// 错误:串行执行,3 次往返
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
// 正确:并行执行,1 次往返
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
])
1.5 策略性 Suspense 边界(Strategic Suspense Boundaries)
影响级别:HIGH(更快的首屏绘制)。不要在异步组件返回 JSX 前 await 数据,而应使用 Suspense 边界让外层 UI 立即展示、数据流式到达:
// 错误:整个页面被数据阻塞
async function Page() {
const data = await fetchData() // 阻塞整个页面
return (
<div>
<div>Sidebar</div><div>Header</div>
<div><DataDisplay data={data} /></div>
<div>Footer</div>
</div>
)
}
// 正确:外层立即渲染,数据流式到达
function Page() {
return (
<div>
<div>Sidebar</div><div>Header</div>
<div>
<Suspense fallback={<Skeleton />}>
<DataDisplay />
</Suspense>
</div>
<div>Footer</div>
</div>
)
}
async function DataDisplay() {
const data = await fetchData() // 只阻塞自身
return <div>{data.content}</div>
}
进阶变体是跨组件共享同一个 Promise:在父组件中立即启动 fetch 但不 await,把 Promise 作为 props 传给多个组件,配合 React 的 use() 解包,多个组件共享一次请求:
function Page() {
const dataPromise = fetchData() // 立即启动,不 await
return (
<div>
<div>Sidebar</div><div>Header</div>
<Suspense fallback={<Skeleton />}>
<DataDisplay dataPromise={dataPromise} />
<DataSummary dataPromise={dataPromise} />
</Suspense>
<div>Footer</div>
</div>
)
}
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise) // 解包 Promise
return <div>{data.content}</div>
}
需要谨慎使用该模式的场景:影响布局决策的关键数据、首屏以上的 SEO 关键内容、小型快速查询(Suspense 开销不值当)、以及希望避免布局位移(loading → 内容跳变)时。本质权衡是"更快的首屏绘制" vs "潜在的布局位移"。
2. 包体积优化(Bundle Size Optimization):CRITICAL
减小初始包体积能直接改善 Time to Interactive(TTI)与 Largest Contentful Paint(LCP)。
2.1 避免 Barrel 文件导入(Avoid Barrel File Imports)
影响级别:CRITICAL(200-800ms 导入成本、构建变慢)。Barrel 文件是指仅做再导出的入口文件(如 export * from './module' 的 index.js)。流行图标与组件库的入口文件可能有高达 10,000 个再导出,许多 React 包仅导入就要花费 200-800ms,同时影响开发速度与生产冷启动。
为什么 tree-shaking 帮不上忙:当库被标记为 external(不打进 bundle)时,打包器无法优化它;若打包以启用 tree-shaking,构建会因分析整个模块图而显著变慢。
// 错误:导入整个库(加载 1,583 个模块,dev 下额外耗时约 2.8s,每次冷启动运行时成本 200-800ms)
import { Check, X, Menu } from 'lucide-react'
// 正确:只导入需要的模块(仅 3 个模块,约 2KB vs 约 1MB)
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+ 提供了 optimizePackageImports 作为替代方案,保留 ergonomic 的 barrel 导入语法,构建时自动转换为直接导入:
// next.config.js
module.exports = {
experimental: {
optimizePackageImports: ['lucide-react']
}
}
// 之后仍可写 ergonomic 导入
import { Check, X, Menu } from 'lucide-react'
文档给出的收益数据:直接导入可带来 15-70% 更快的 dev 启动、28% 更快的构建、40% 更快的冷启动以及显著更快的 HMR。受影响最典型的库包括 lucide-react、@tabler/icons-react、react-icons、@radix-ui/react-*、lodash、ramda、date-fns、rxjs、react-use 等。langfuse 前端恰好使用 react-icons(5.5.0,见 web/package.json),重构时优先从具体路径导入图标,可避免引入大量无关模块。
2.2 条件模块加载(Conditional Module Loading)
影响级别:HIGH(仅在需要时加载大数据)。只有功能被激活时才加载大数据或模块:
function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {
const [frames, setFrames] = useState<Frame[] | null>(null)
useEffect(() => {
if (enabled && !frames && typeof window !== 'undefined') {
import('./animation-frames.js')
.then(mod => setFrames(mod.frames))
.catch(() => setEnabled(false))
}
}, [enabled, frames, setEnabled])
if (!frames) return <Skeleton />
return <Canvas frames={frames} />
}
其中的 typeof window !== 'undefined' 检查会阻止该模块被打入 SSR bundle,从而优化服务端包体积与构建速度。
2.3 延迟非关键第三方库(Defer Non-Critical Third-Party Libraries)
影响级别:MEDIUM(水合后加载)。分析、日志、错误追踪不阻塞用户交互,应在水合之后再加载:
import dynamic from 'next/dynamic'
const Analytics = dynamic(
() => import('@vercel/analytics/react').then(m => m.Analytics),
{ ssr: false }
)
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics /> {/* 水合后才加载 */}
</body>
</html>
)
}
2.4 重型组件使用动态导入(Dynamic Imports for Heavy Components)
影响级别:CRITICAL(直接影响 TTI 与 LCP)。用 next/dynamic 懒加载首屏不需要的大组件——例如 Monaco 编辑器(文档指出直接打包会让主 chunk 增加约 300KB):
import dynamic from 'next/dynamic'
const MonacoEditor = dynamic(
() => import('./monaco-editor').then(m => m.MonacoEditor),
{ ssr: false }
)
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />
}
langfuse 前端正是这样做的:在 web/src/components/layouts/app-layout/variants/AuthenticatedLayout.tsx 中,CommandMenu、PaymentBanner、PreviewDeploymentBanner 等非首屏必需组件全部通过 next/dynamic 延迟加载,避免它们进入初始 bundle。
2.5 基于用户意图预加载(Preload Based on User Intent)
影响级别:MEDIUM(降低感知延迟)。在需要之前预加载重型 bundle:
function EditorButton({ onClick }: { onClick: () => void }) {
const preload = () => {
if (typeof window !== 'undefined') {
void import('./monaco-editor')
}
}
return (
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
Open Editor
</button>
)
}
也可以结合特性开关预加载:当 flags.editorEnabled 为真时立即执行 void import('./monaco-editor').then(mod => mod.init())。同样地,typeof window !== 'undefined' 检查可避免预加载模块进入 SSR bundle。
3. 服务端性能(Server-Side Performance):HIGH
优化服务端渲染与数据获取,消除服务端瀑布流并降低响应时间。
3.1 像 API 路由一样认证 Server Actions
影响级别:CRITICAL(防止未授权访问服务端变更)。带 "use server" 的 Server Actions 与 API 路由一样是公开端点,必须在每个 Server Action 内部校验认证与授权,不能只依赖 middleware、布局守卫或页面级检查——Server Actions 可被直接调用。Next.js 官方文档明确要求:"以对待公开 API 端点同样的安全考量对待 Server Actions,并验证用户是否被允许执行变更。"
'use server'
import { verifySession } from '@/lib/auth'
import { unauthorized } from '@/lib/errors'
export async function deleteUser(userId: string) {
// 始终在 action 内部校验认证
const session = await verifySession()
if (!session) {
throw unauthorized('Must be logged in')
}
// 同时校验授权
if (session.user.role !== 'admin' && session.user.id !== userId) {
throw unauthorized('Cannot delete other users')
}
await db.user.delete({ where: { id: userId } })
return { success: true }
}
标准顺序是:先输入校验(如 zod schema)→ 再认证 → 再授权 → 最后执行变更:
'use server'
import { z } from 'zod'
const updateProfileSchema = z.object({
userId: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email()
})
export async function updateProfile(data: unknown) {
const validated = updateProfileSchema.parse(data) // 1. 先校验输入
const session = await verifySession() // 2. 再认证
if (!session) throw new Error('Unauthorized')
if (session.user.id !== validated.userId) { // 3. 再授权
throw new Error('Can only update own profile')
}
await db.user.update({ // 4. 最后变更
where: { id: validated.userId },
data: { name: validated.name, email: validated.email }
})
return { success: true }
}
3.2 避免 RSC Props 中的重复序列化
影响级别:LOW。RSC→client 的序列化去重基于对象引用而非值:相同引用只序列化一次,新引用会再次序列化。因此应在客户端做变换(.toSorted()、.filter()、.map()),而不是服务端:
// 错误:RSC 发送 6 个字符串(2 个数组 × 3 项)
<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
// 正确:RSC 只发送一次
<ClientList usernames={usernames} />
// 客户端再变换
'use client'
const sorted = useMemo(() => [...usernames].sort(), [usernames])
去重是递归的,影响因数据类型而异:string[]/number[]/boolean[] 影响高(数组与全部原始值被完全复制);object[] 影响低(数组被复制,但嵌套对象按引用去重)。破坏去重的操作都是"创建新引用"的操作:数组的 .toSorted()、.filter()、.map()、.slice()、[...arr],对象的 {...obj}、Object.assign()、structuredClone()、JSON.parse(JSON.stringify())。例外情况:当变换很昂贵或客户端不需要原始数据时,可以传派生数据。
3.3 跨请求 LRU 缓存(Cross-Request LRU Caching)
影响级别:HIGH(跨请求缓存)。React.cache() 只在单个请求内有效;对于跨连续请求共享的数据(用户点击按钮 A 再点击按钮 B),应使用 LRU 缓存:
import { LRUCache } from 'lru-cache'
const cache = new LRUCache<string, any>({
max: 1000,
ttl: 5 * 60 * 1000 // 5 分钟
})
export async function getUser(id: string) {
const cached = cache.get(id)
if (cached) return cached
const user = await db.user.findUnique({ where: { id } })
cache.set(id, user)
return user
}
// 请求 1:DB 查询并缓存;请求 2:缓存命中,无 DB 查询
在 Vercel 的 Fluid Compute 下效果尤其好:多个并发请求共享同一函数实例与缓存,无需 Redis 等外部存储;而在传统 serverless 中每次调用相互隔离,跨进程缓存需考虑 Redis。
3.4 最小化 RSC 边界序列化
影响级别:HIGH(降低传输体积)。RSC/Client 边界会把对象所有属性序列化为字符串并嵌入 HTML 响应与后续 RSC 请求,直接影响页面重量与加载时间。只传客户端真正用到的字段:
// 错误:序列化全部 50 个字段
async function Page() {
const user = await fetchUser() // 50 个字段
return <Profile user={user} />
}
'use client'
function Profile({ user }: { user: User }) {
return <div>{user.name}</div> // 只用 1 个字段
}
// 正确:只序列化 1 个字段
async function Page() {
const user = await fetchUser()
return <Profile name={user.name} />
}
'use client'
function Profile({ name }: { name: string }) {
return <div>{name}</div>
}
3.5 组件组合实现并行数据获取
影响级别:CRITICAL(消除服务端瀑布流)。React Server Components 在组件树内是串行执行的,通过组合重构来并行化数据获取:
// 错误:Sidebar 等待 Page 的 fetch 完成
export default async function Page() {
const header = await fetchHeader()
return (
<div>
<div>{header}</div>
<Sidebar />
</div>
)
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
}
// 正确:两者同时 fetch
async function Header() {
const data = await fetchHeader()
return <div>{data}</div>
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
}
export default function Page() {
return (
<div>
<Header />
<Sidebar />
</div>
)
}
另一种等价方案是用 children prop 组合:把 Sidebar 作为 children 传入 Layout,使其与 Header 并行执行。
3.6 用 React.cache() 做请求内去重
影响级别:MEDIUM(请求内去重)。认证与数据库查询最受益:
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 }
})
})
单个请求内多次调用 getCurrentUser() 只执行一次查询。关键陷阱:React.cache() 用浅比较(Object.is)判断缓存命中,内联对象每次调用都会创建新引用导致永远缓存未命中。若必须传对象,请传同一引用:
const params = { uid: 1 }
getUser(params) // 查询执行
getUser(params) // 缓存命中(同一引用)
Next.js 专属提示:Next.js 已为 fetch 自动扩展请求记忆化(同 URL 同选项的请求在单请求内自动去重),无需对 fetch 使用 React.cache();但它对数据库查询(Prisma、Drizzle 等)、重计算、认证检查、文件系统操作等非 fetch 异步工作仍然不可或缺。
3.7 用 after() 处理非阻塞操作
影响级别:MEDIUM(更快的响应时间)。用 Next.js 的 after() 把应在响应发送后执行的工作排到后面,避免日志、分析等副作用阻塞响应:
import { after } from 'next/server'
import { headers, cookies } from 'next/headers'
import { logUserAction } from '@/app/utils'
export async function POST(request: Request) {
await updateDatabase(request) // 先执行变更
// 响应发送后再记录日志
after(async () => {
const userAgent = (await headers()).get('user-agent') || 'unknown'
const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
logUserAction({ sessionCookie, userAgent })
})
return new Response(JSON.stringify({ status: 'success' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
}
典型用途:分析追踪、审计日志、发送通知、缓存失效、清理任务。重要说明:after() 即使响应失败或重定向也会执行;它适用于 Server Actions、Route Handlers 与 Server Components。langfuse 当前使用的 Next.js 16.3.3 完全支持该 API。
4. 客户端数据获取(Client-Side Data Fetching):MEDIUM-HIGH
自动去重与高效的数据获取模式能减少冗余网络请求。
4.1 去重全局事件监听器
影响级别:LOW(N 个组件 → 1 个监听器)。用 useSWRSubscription() 在组件实例间共享全局事件监听器,配合模块级 Map 按 key 管理回调:
import useSWRSubscription from 'swr/subscription'
// 模块级 Map,按 key 跟踪回调
const keyCallbacks = new Map<string, Set<() => void>>()
function useKeyboardShortcut(key: string, callback: () => void) {
useEffect(() => {
if (!keyCallbacks.has(key)) keyCallbacks.set(key, new Set())
keyCallbacks.get(key)!.add(callback)
return () => {
const set = keyCallbacks.get(key)
if (set) {
set.delete(callback)
if (set.size === 0) keyCallbacks.delete(key)
}
}
}, [key, callback])
useSWRSubscription('global-keydown', () => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && keyCallbacks.has(e.key)) {
keyCallbacks.get(e.key)!.forEach(cb => cb())
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
})
}
4.2 滚动性能用被动事件监听器
影响级别:MEDIUM(消除事件监听器导致的滚动延迟)。给 touch 与 wheel 事件监听器加 { passive: true } 以支持即时滚动。浏览器默认会等待监听器执行完毕以检查是否调用 preventDefault(),从而造成滚动延迟:
useEffect(() => {
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
document.addEventListener('touchstart', handleTouch, { passive: true })
document.addEventListener('wheel', handleWheel, { passive: true })
return () => {
document.removeEventListener('touchstart', handleTouch)
document.removeEventListener('wheel', handleWheel)
}
}, [])
适用 passive 的场景:埋点/分析、日志、任何不调用 preventDefault() 的监听器。不适用场景:实现自定义滑动手势、自定义缩放控制等需要 preventDefault() 的监听器。
4.3 用 SWR 实现自动去重
影响级别:MEDIUM-HIGH(自动去重)。SWR 支持跨组件实例的请求去重、缓存与重新验证:
import useSWR from 'swr'
// 多个实例共享一次请求
function UserList() {
const { data: users } = useSWR('/api/users', fetcher)
}
// 不可变数据
import { useImmutableSWR } from '@/lib/swr'
function StaticContent() {
const { data } = useImmutableSWR('/api/config', fetcher)
}
// 变更
import { useSWRMutation } from 'swr/mutation'
function UpdateButton() {
const { trigger } = useSWRMutation('/api/user', updateUser)
return <button onClick={() => trigger()}>Update</button>
}
值得说明的是,langfuse 前端并未使用 SWR,而是采用功能等价的 @tanstack/react-query(^5.85.1,见 web/package.json)并通过 tRPC 集成实现跨组件的查询去重、缓存与失效——本规则的核心思想(去重、缓存、重新验证)在两个生态中完全通用。
4.4 版本化并最小化 localStorage 数据
影响级别:MEDIUM(防止 schema 冲突、减小存储体积)。给 key 加版本前缀,只存需要的字段:
const VERSION = 'v2'
function saveConfig(config: { theme: string; language: string }) {
try {
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
} catch {
// 隐私模式/配额超限/被禁用时会抛错
}
}
function loadConfig() {
try {
const data = localStorage.getItem(`userConfig:${VERSION}`)
return data ? JSON.parse(data) : null
} catch {
return null
}
}
// v1 → v2 迁移
function migrate() {
try {
const v1 = localStorage.getItem('userConfig:v1')
if (v1) {
const old = JSON.parse(v1)
saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })
localStorage.removeItem('userConfig:v1')
}
} catch {}
}
从服务端响应只缓存 UI 需要的字段(如 20+ 字段的 user 对象只存 theme 与 notifications)。始终用 try-catch 包裹:getItem()/setItem() 在 Safari、Firefox 的隐私模式下、配额超限或存储被禁用时会抛错。收益:通过版本号实现 schema 演进、减小存储体积、避免存储 token/PII/内部标志。
5. 重渲染优化(Re-render Optimization):MEDIUM
减少不必要的重渲染,降低浪费的计算并提升 UI 响应性。
5.1 渲染期间计算派生状态
若某个值可由当前 props/state 计算得出,就不要存入 state 或通过 effect 更新。渲染期派生可避免额外渲染与状态漂移:
// 错误:冗余的 state + effect
function Form() {
const [firstName, setFirstName] = useState('First')
const [lastName, setLastName] = useState('Last')
const [fullName, setFullName] = useState('')
useEffect(() => {
setFullName(firstName + ' ' + lastName)
}, [firstName, lastName])
return <p>{fullName}</p>
}
// 正确:渲染期派生
function Form() {
const [firstName, setFirstName] = useState('First')
const [lastName, setLastName] = useState('Last')
const fullName = firstName + ' ' + lastName
return <p>{fullName}</p>
}
不要仅仅为了响应 prop 变化而在 effect 中 setState;应优先派生值或使用 keyed reset。
5.2 延迟状态读取到使用点
如果只在回调内读取动态状态(searchParams、localStorage),就不要订阅它:
// 错误:订阅所有 searchParams 变化
function ShareButton({ chatId }: { chatId: string }) {
const searchParams = useSearchParams()
const handleShare = () => {
const ref = searchParams.get('ref')
shareChat(chatId, { ref })
}
return <button onClick={handleShare}>Share</button>
}
// 正确:按需读取,无订阅
function ShareButton({ chatId }: { chatId: string }) {
const handleShare = () => {
const params = new URLSearchParams(window.location.search)
const ref = params.get('ref')
shareChat(chatId, { ref })
}
return <button onClick={handleShare}>Share</button>
}
5.3 简单原始类型表达式不要包 useMemo
当表达式简单(少数逻辑或算术运算符)且结果为原始类型(boolean、number、string)时,不要包 useMemo——调用 useMemo 并比较 hook 依赖本身可能比表达式更消耗资源:
// 错误
const isLoading = useMemo(() => {
return user.isLoading || notifications.isLoading
}, [user.isLoading, notifications.isLoading])
// 正确
const isLoading = user.isLoading || notifications.isLoading
5.4 将 memo 组件的非原始默认参数提取为常量
memo 组件若给某非原始可选参数(数组、函数、对象)设默认值,调用时省略该参数会破坏 memo 化——每次重渲染都会创建新值实例,无法通过 memo() 的严格相等比较:
// 错误:每次重渲染 onClick 值都不同
const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
// ...
})
<UserAvatar /> // 省略可选 onClick 时 memo 失效
// 正确:稳定的默认值
const NOOP = () => {};
const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
// ...
})
5.5 提取为 memo 组件
把昂贵工作提取到 memo 组件中,从而在计算前支持提前返回:
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const id = useMemo(() => computeAvatarId(user), [user])
return <Avatar id={id} />
})
function Profile({ user, loading }: Props) {
if (loading) return <Skeleton /> // 提前返回,跳过计算
return (
<div>
<UserAvatar user={user} />
</div>
)
}
注意:若项目启用了 React Compiler,memo() 与 useMemo() 的手动记忆化就不必要了,编译器会自动优化重渲染。
5.6 收窄 effect 依赖
指定原始类型依赖而非对象,以最小化 effect 重跑:
// 错误:user 的任何字段变化都重跑
useEffect(() => { console.log(user.id) }, [user])
// 正确:仅 id 变化时重跑
useEffect(() => { console.log(user.id) }, [user.id])
// 派生状态在 effect 外计算
const isMobile = width < 768 // 只在布尔值切换时触发
useEffect(() => {
if (isMobile) enableMobileMode()
}, [isMobile])
5.7 交互逻辑放入事件处理器
如果副作用由特定用户动作(提交、点击、拖拽)触发,就直接在事件处理器中执行,不要把动作建模为"state + effect":
// 错误:事件建模为 state + effect
function Form() {
const [submitted, setSubmitted] = useState(false)
const theme = useContext(ThemeContext)
useEffect(() => {
if (submitted) {
post('/api/register')
showToast('Registered', theme)
}
}, [submitted, theme])
return <button onClick={() => setSubmitted(true)}>Submit</button>
}
// 正确:在处理器中执行
function Form() {
const theme = useContext(ThemeContext)
function handleSubmit() {
post('/api/register')
showToast('Registered', theme)
}
return <button onClick={handleSubmit}>Submit</button>
}
5.8 订阅派生状态
订阅派生布尔状态而非连续值,减少重渲染频率:
// 错误:每个像素变化都重渲染
function Sidebar() {
const width = useWindowWidth() // 连续更新
const isMobile = width < 768
return <nav className={isMobile ? 'mobile' : 'desktop'} />
}
// 正确:仅布尔值变化时重渲染
function Sidebar() {
const isMobile = useMediaQuery('(max-width: 767px)')
return <nav className={isMobile ? 'mobile' : 'desktop'} />
}
5.9 使用函数式 setState 更新
基于当前 state 更新时,用函数式更新而非直接引用 state 变量,可防止陈旧闭包、消除不必要依赖、创建稳定回调引用:
// 错误:回调必须依赖 items,每次 items 变化都重建;或缺失依赖导致陈旧闭包
function TodoList() {
const [items, setItems] = useState(initialItems)
const addItems = useCallback((newItems: Item[]) => {
setItems([...items, ...newItems])
}, [items]) // items 依赖导致反复重建
const removeItem = useCallback((id: string) => {
setItems(items.filter(item => item.id !== id))
}, []) // 缺失 items 依赖,将始终引用初始值——陈旧闭包 bug!
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
}
// 正确:稳定回调、无陈旧闭包
function TodoList() {
const [items, setItems] = useState(initialItems)
const addItems = useCallback((newItems: Item[]) => {
setItems(curr => [...curr, ...newItems])
}, []) // 无需依赖
const removeItem = useCallback((id: string) => {
setItems(curr => curr.filter(item => item.id !== id))
}, []) // 始终使用最新 state
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
}
收益:稳定回调引用、无陈旧闭包、更少的依赖数组、消除最常见的 React 闭包 bug 来源。适用场景:任何依赖当前 state 的 setState、useCallback/useMemo 内需要 state、引用 state 的事件处理器、异步更新 state。直接更新(setCount(0)、setName(newName))在静态值或仅由 props/参数决定时是没问题的。即使启用 React Compiler,函数式更新仍推荐用于正确性与防陈旧闭包。
5.10 惰性 state 初始化
昂贵的初始值请向 useState 传函数。不用函数形式时,初始化器在每次渲染都会执行,即使值只使用一次:
// 错误:每次渲染都执行
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
const [settings, setSettings] = useState(JSON.parse(localStorage.getItem('settings') || '{}'))
// 正确:仅初始渲染执行一次
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
const [settings, setSettings] = useState(() => {
const stored = localStorage.getItem('settings')
return stored ? JSON.parse(stored) : {}
})
适用场景:从 localStorage/sessionStorage 计算初始值、构建数据结构(索引、Map)、读取 DOM、重型变换。简单原始值(useState(0))、直接引用(useState(props.value))或廉价字面量(useState({}))不需要函数形式。
这一模式在 langfuse 前端已被实际使用:例如 web/src/components/editor/mediaTagWidget.tsx 中的 useState(() => new MediaTagWidgetStore()) 与 web/src/components/ChatMessages/messageSearch/context.tsx 中的 useState(() => ...) 都通过惰性初始化避免在每次渲染时重新构造实例。
5.11 非紧急更新使用 Transitions
把频繁、非紧急的 state 更新标记为 transition,保持 UI 响应性:
import { startTransition } from 'react'
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
useEffect(() => {
const handler = () => {
startTransition(() => setScrollY(window.scrollY))
}
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
}
5.12 瞬时值使用 useRef
值频繁变化且不想每次更新都触发重渲染时(鼠标追踪、定时器、瞬时标志),存入 useRef。组件 state 留给 UI,ref 用于临时 DOM 相关值:
function Tracker() {
const lastXRef = useRef(0)
const dotRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const onMove = (e: MouseEvent) => {
lastXRef.current = e.clientX
const node = dotRef.current
if (node) {
node.style.transform = `translateX(${e.clientX}px)` // 直接操作 DOM,不触发重渲染
}
}
window.addEventListener('mousemove', onMove)
return () => window.removeEventListener('mousemove', onMove)
}, [])
return (
<div ref={dotRef} style={{ position: 'fixed', top: 0, left: 0, width: 8, height: 8, background: 'black', transform: 'translateX(0px)' }} />
)
}
6. 渲染性能(Rendering Performance):MEDIUM
优化渲染过程,减少浏览器的工作量。
6.1 动画 SVG 包装层而非 SVG 元素
很多浏览器对 SVG 元素上的 CSS3 动画没有硬件加速。把 SVG 包在 <div> 中,动画包装层:
function LoadingSpinner() {
return (
<div className="animate-spin"> {/* 动画作用于 div,可 GPU 加速 */}
<svg width="24" height="24" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" />
</svg>
</div>
)
}
适用于所有 CSS transform 与 transition(transform、opacity、translate、scale、rotate)。
6.2 长列表使用 CSS content-visibility
对屏外内容应用 content-visibility: auto 延迟渲染。文档示例:1000 条消息时,浏览器跳过约 990 条屏外项的布局/绘制(首屏渲染快约 10 倍):
.message-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}
function MessageList({ messages }: { messages: Message[] }) {
return (
<div className="overflow-y-auto h-screen">
{messages.map(msg => (
<div key={msg.id} className="message-item">
<Avatar user={msg.author} />
<div>{msg.content}</div>
</div>
))}
</div>
)
}
6.3 提升静态 JSX 元素
把静态 JSX 提取到组件外,避免每次渲染重新创建:
// 错误:每次渲染都重新创建元素
function LoadingSkeleton() {
return <div className="animate-pulse h-20 bg-gray-200" />
}
function Container() {
return (
<div>
{loading && <LoadingSkeleton />}
</div>
)
}
// 正确:复用同一元素
const loadingSkeleton = (
<div className="animate-pulse h-20 bg-gray-200" />
)
function Container() {
return (
<div>
{loading && loadingSkeleton}
</div>
)
}
对大型静态 SVG 节点尤其有效。若启用 React Compiler,编译器会自动提升静态 JSX 并优化重渲染,手动提升不再必要。
6.4 优化 SVG 精度
降低 SVG 坐标精度可减小文件体积,最优精度取决于 viewBox 尺寸:
<!-- 错误:过高精度 -->
<path d="M 10.293847 20.847362 L 30.938472 40.192837" />
<!-- 正确:1 位小数 -->
<path d="M 10.3 20.8 L 30.9 40.2" />
可用 SVGO 自动化:npx svgo --precision=1 --multipass icon.svg。
6.5 无闪烁地防止水合不匹配
渲染依赖客户端存储(localStorage、cookies)的内容时,注入一个同步脚本在 React 水合前更新 DOM,同时避免 SSR 崩溃与闪烁:
function ThemeWrapper({ children }: { children: ReactNode }) {
return (
<>
<div id="theme-wrapper">
{children}
</div>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
var theme = localStorage.getItem('theme') || 'light';
var el = document.getElementById('theme-wrapper');
if (el) el.className = theme;
} catch (e) {}
})();
`,
}}
/>
</>
)
}
内联脚本在元素展示前同步执行,确保 DOM 已有正确值——无闪烁、无水合不匹配。此模式特别适合主题切换、用户偏好、认证状态等任何应立即渲染而不闪默认值的客户端数据。
6.6 抑制预期中的水合不匹配
对 SSR 框架中有意在服务端与客户端不同的值(随机 ID、日期、本地化/时区格式),用 suppressHydrationWarning 包裹动态文本以消除噪音警告。不要用它掩盖真实 bug,也不要过度使用:
// 已知不匹配:时间戳
function Timestamp() {
return (
<span suppressHydrationWarning>
{new Date().toLocaleString()}
</span>
)
}
6.7 显隐切换使用 Activity 组件
对频繁切换可见性的昂贵组件,用 React 的 <Activity> 保留 state/DOM:
import { Activity } from 'react'
function Dropdown({ isOpen }: Props) {
return (
<Activity mode={isOpen ? 'visible' : 'hidden'}>
<ExpensiveMenu />
</Activity>
)
}
避免昂贵的重渲染与状态丢失。
6.8 使用显式条件渲染
当条件可能为 0、NaN 等会被渲染出来的 falsy 值时,用三元表达式(? :)替代 &&:
// 错误:count 为 0 时渲染出 "0"
function Badge({ count }: { count: number }) {
return (
<div>
{count && <span className="badge">{count}</span>}
</div>
)
}
// 正确:count 为 0 时什么都不渲染
function Badge({ count }: { count: number }) {
return (
<div>
{count > 0 ? <span className="badge">{count}</span> : null}
</div>
)
}
6.9 用 useTransition 替代手动加载状态
用 useTransition 内置的 isPending 状态替代手动的 useState 加载状态:
import { useTransition, useState } from 'react'
function SearchResults() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
const handleSearch = (value: string) => {
setQuery(value) // 立即更新输入框
startTransition(async () => {
const data = await fetchResults(value)
setResults(data)
})
}
return (
<>
<input onChange={(e) => handleSearch(e.target.value)} />
{isPending && <Spinner />}
<ResultsList results={results} />
</>
)
}
收益:自动 pending 状态(无需手动管理 setIsLoading(true/false))、错误韧性(transition 抛出时 pending 状态也能正确重置)、更好的响应性、中断处理(新 transition 自动取消未完成的)。
7. JavaScript 性能:LOW-MEDIUM
热路径的微优化累积起来也能带来可观的改进。
7.1 避免布局抖动(Layout Thrashing)
避免在样式写入之间穿插布局读取。在样式更改之间读取布局属性(offsetWidth、getBoundingClientRect()、getComputedStyle())会强制浏览器触发同步回流:
// 错误:读写交错,强制多次回流
function layoutThrashing(element: HTMLElement) {
element.style.width = '100px'
const width = element.offsetWidth // 强制回流
element.style.height = '200px'
const height = element.offsetHeight // 再次强制回流
}
// 正确:先批量写入,再一次性读取(单次回流)
function updateElementStyles(element: HTMLElement) {
element.style.width = '100px'
element.style.height = '200px'
element.style.backgroundColor = 'blue'
element.style.border = '1px solid black'
const { width, height } = element.getBoundingClientRect()
}
React 场景中更推荐使用 CSS 类而非内联样式(CSS 文件可被浏览器缓存、关注点分离、更易维护):
function Box({ isHighlighted }: { isHighlighted: boolean }) {
return (
<div className={isHighlighted ? 'highlighted-box' : ''}>
Content
</div>
)
}
7.2 重复查找建立索引 Map
同一 key 的多次 .find() 应改用 Map(1000 订单 × 1000 用户:100 万次操作 → 2000 次操作):
// 错误:每次查找 O(n)
function processOrders(orders: Order[], users: User[]) {
return orders.map(order => ({
...order,
user: users.find(u => u.id === order.userId)
}))
}
// 正确:每次查找 O(1)
function processOrders(orders: Order[], users: User[]) {
const userById = new Map(users.map(u => [u.id, u]))
return orders.map(order => ({
...order,
user: userById.get(order.userId)
}))
}
7.3 循环内缓存属性访问
// 错误:N 次迭代 × 3 次查找
for (let i = 0; i < arr.length; i++) {
process(obj.config.settings.value)
}
// 正确:总计 1 次查找
const value = obj.config.settings.value
const len = arr.length
for (let i = 0; i < len; i++) {
process(value)
}
7.4 缓存重复函数调用
同一输入反复调用同一函数时,用模块级 Map 缓存结果:
const slugifyCache = new Map<string, string>()
function cachedSlugify(text: string): string {
if (slugifyCache.has(text)) return slugifyCache.get(text)!
const result = slugify(text)
slugifyCache.set(text, result)
return result
}
function ProjectList({ projects }: { projects: Project[] }) {
return (
<div>
{projects.map(project => {
const slug = cachedSlugify(project.name) // 同名只计算一次
return <ProjectCard key={project.id} slug={slug} />
})}
</div>
)
}
单值函数可用更简单的模块级变量缓存,并在 auth 变化时置空。用 Map(而非 hook)使其在工具函数、事件处理器等 React 组件之外也能工作。
7.5 缓存 Storage API 调用
localStorage、sessionStorage、document.cookie 是同步且昂贵的,把读取缓存在内存中:
const storageCache = new Map<string, string | null>()
function getLocalStorage(key: string) {
if (!storageCache.has(key)) {
storageCache.set(key, localStorage.getItem(key))
}
return storageCache.get(key)
}
function setLocalStorage(key: string, value: string) {
localStorage.setItem(key, value)
storageCache.set(key, value) // 保持缓存同步
}
Cookie 缓存:document.cookie.split('; ') 解析为对象后缓存。重要:外部变化时失效——监听 storage 事件(其他标签页)与 visibilitychange(回到可见时)清空缓存。
7.6 合并多次数组遍历
多个 .filter()/.map() 会多次遍历数组,合并为一次循环:
// 错误:3 次遍历
const admins = users.filter(u => u.isAdmin)
const testers = users.filter(u => u.isTester)
const inactive = users.filter(u => !u.isActive)
// 正确:1 次遍历
const admins: User[] = []
const testers: User[] = []
const inactive: User[] = []
for (const user of users) {
if (user.isAdmin) admins.push(user)
if (user.isTester) testers.push(user)
if (!user.isActive) inactive.push(user)
}
7.7 数组比较先检查长度
用昂贵操作(排序、深比较、序列化)比较数组时先查长度;长度不同数组必不相等。这避免了长度不同时仍执行两个 O(n log n) 排序,也避免了 join 字符串的内存消耗与对原数组的修改:
function hasChanges(current: string[], original: string[]) {
// 先做 O(1) 长度检查
if (current.length !== original.length) {
return true
}
// 长度相同才排序比较,找到差异立即返回
const currentSorted = current.toSorted()
const originalSorted = original.toSorted()
for (let i = 0; i < currentSorted.length; i++) {
if (currentSorted[i] !== originalSorted[i]) {
return true
}
}
return false
}
7.8 函数提前返回
结果已确定时提前返回,跳过不必要处理:
// 错误:找到错误后仍继续处理所有项
function validateUsers(users: User[]) {
let hasError = false
let errorMessage = ''
for (const user of users) {
if (!user.email) { hasError = true; errorMessage = 'Email required' }
if (!user.name) { hasError = true; errorMessage = 'Name required' }
}
return hasError ? { valid: false, error: errorMessage } : { valid: true }
}
// 正确:第一个错误立即返回
function validateUsers(users: User[]) {
for (const user of users) {
if (!user.email) return { valid: false, error: 'Email required' }
if (!user.name) return { valid: false, error: 'Name required' }
}
return { valid: true }
}
7.9 提升 RegExp 创建
不要在 render 内创建 RegExp,提升到模块作用域或用 useMemo():
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
function Highlighter({ text, query }: Props) {
const regex = useMemo(
() => new RegExp(`(${escapeRegex(query)})`, 'gi'),
[query]
)
const parts = text.split(regex)
return <>{parts.map((part, i) => ...)}</>
}
警告:全局正则(/g)有可变的 lastIndex 状态,regex.test('foo') 第一次返回 true 后 lastIndex 变为 3,第二次再调用同一正则可能返回 false——复用全局正则时要特别注意。
7.10 找 Min/Max 用循环而非排序
找最小或最大元素只需一次遍历,排序是浪费且更慢(O(n) 替代 O(n log n)):
function getLatestProject(projects: Project[]) {
if (projects.length === 0) return null
let latest = projects[0]
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt > latest.updatedAt) {
latest = projects[i]
}
}
return latest
}
function getOldestAndNewest(projects: Project[]) {
if (projects.length === 0) return { oldest: null, newest: null }
let oldest = projects[0]
let newest = projects[0]
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
}
return { oldest, newest }
}
小数组可用 Math.min(...numbers)/Math.max(...numbers),但展开运算符对大数组有限制(文档提及 Chrome 143 约为 124000、Safari 18 约为 638000 的最大数组长度),为可靠性建议用循环。
7.11 用 Set/Map 实现 O(1) 查找
数组转 Set/Map 以进行重复成员检查:
// 错误:每次检查 O(n)
const allowedIds = ['a', 'b', 'c', ...]
items.filter(item => allowedIds.includes(item.id))
// 正确:每次检查 O(1)
const allowedIds = new Set(['a', 'b', 'c', ...])
items.filter(item => allowedIds.has(item.id))
7.12 用 toSorted() 代替 sort() 保证不可变
.sort() 原地修改数组,会破坏 React 的 state 与 props 不可变性模型,并可能引发陈旧闭包 bug。用 .toSorted() 创建新数组:
function UserList({ users }: { users: User[] }) {
// 创建新排序数组,原数组不变
const sorted = useMemo(
() => users.toSorted((a, b) => a.name.localeCompare(b.name)),
[users]
)
return <div>{sorted.map(renderUser)}</div>
}
.toSorted() 在 Chrome 110+、Safari 16+、Firefox 115+、Node.js 20+ 可用;旧环境回退:const sorted = [...items].sort((a, b) => a.value - b.value)。其他不可变数组方法还有 .toReversed()、.toSpliced()、.with()。
这一规则在 langfuse 前端已有落地:如 web/src/components/editor/CodeMirrorEditor.tsx、web/src/features/monitors/components/MonitorsTable.tsx 以及服务端测试 web/src/tests/server/queryBuilder.servertest.ts 等文件中均使用了 toSorted() 等不可变数组方法,避免在排序时修改 React 状态与查询参数数组。
8. 高级模式(Advanced Patterns):LOW
适用于特定场景、需要谨慎实现的高级模式。
8.1 应用初始化一次,而非每次挂载
不要把必须每次应用加载只执行一次的全局初始化放进组件 useEffect([])——组件会重挂载导致 effect 重跑。用模块级守卫或入口模块的顶层初始化:
let didInit = false
function Comp() {
useEffect(() => {
if (didInit) return
didInit = true
loadFromStorage()
checkAuthToken()
}, [])
// ...
}
8.2 事件处理器存入 Refs
当 effect 不应因回调变化而重新订阅时,把回调存入 refs:
import { useEffectEvent } from 'react'
function useWindowEvent(event: string, handler: (e) => void) {
const onEvent = useEffectEvent(handler) // 稳定的函数引用,始终调用最新 handler
useEffect(() => {
window.addEventListener(event, onEvent)
return () => window.removeEventListener(event, onEvent)
}, [event]) // 不依赖 handler,不会反复重订阅
}
useEffectEvent 创建稳定函数引用,始终调用最新版本的 handler。
8.3 useEffectEvent 提供稳定回调引用
在回调中访问最新值而不把它们加入依赖数组,防止 effect 重跑同时避免陈旧闭包:
import { useEffectEvent } from 'react';
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('')
const onSearchEvent = useEffectEvent(onSearch)
useEffect(() => {
const timeout = setTimeout(() => onSearchEvent(query), 300)
return () => clearTimeout(timeout)
}, [query]) // onSearch 变化不再触发 effect 重跑
}
在 langfuse 前端中的印证与实践建议
这套规则不是纸上谈兵,langfuse 前端本身就是很好的印证样本:
- 技术栈契合度高:web/package.json 中的
next: 16.3.3与react: 19.2.4对文档中依赖新 API 的规则(after()、useEffectEvent、Activity、use()解包 Promise、toSorted()等)提供完整支持; - 动态导入已落地:web/src/components/layouts/app-layout/variants/AuthenticatedLayout.tsx 中对 CommandMenu、PaymentBanner、PreviewDeploymentBanner 使用
next/dynamic,对应 2.4 节的重型组件延迟加载; - 惰性初始化已落地:web/src/components/editor/mediaTagWidget.tsx 与 web/src/components/ChatMessages/messageSearch/context.tsx 使用
useState(() => ...),对应 5.10 节; - 不可变数组操作已落地:web/src/components/editor/CodeMirrorEditor.tsx、web/src/features/monitors/components/MonitorsTable.tsx 使用
toSorted(),对应 7.12 节; - 数据获取去重:langfuse 以
@tanstack/react-query(^5.85.1)+ tRPC +superjson(2.2.2)承担 4.3 节 SWR 所要解决的问题——跨组件请求去重、缓存与重新验证; - 图标库注意:项目使用
react-icons(5.5.0),属于文档 2.1 节列出的高导入成本库,重构图标引入时应优先考虑直接路径导入或optimizePackageImports。
实践建议:把本文的 40+ 条规则作为代码评审与自动化重构的检查清单,按优先级排序处理——先消除服务端与客户端瀑布流、削减 bundle,再落实服务端缓存与序列化最小化,最后再处理重渲染与 JS 微观优化。每条规则对应的独立详解文件均位于 web/.agents/skills/vercel-react-best-practices/rules/ 目录,命名规则为前缀 + 主题(如 async-parallel.md、bundle-barrel-imports.md、server-cache-react.md、js-tosorted-immutable.md),便于按图索骥、逐条落地。