Vercel React & Next.js 最佳实践指南
简介:本文档基于 Vercel 工程团队为 AI 编程助手(Agent)制定的
react-best-practices标准。这套标准将 40 多条工程规则按优先级分类,旨在指导开发者编写默认高性能、健壮的现代 Web 应用。
有没有过工作了好多年,知识方面业务方面懂的很多了,但是编程的技巧没有提高多少,现在好消息来了!
最近vercel公司发布了一系列skills,其中一个就是教你怎么写react和NextJS的(react-best-practices),vercel是收编了react团队和Next的公司。所以它的skill可以说是官方指导。我把它翻译成了中文。
虽然说skill是指导ai编程的,但是如果你学会了,以后在审阅ai生成的代码时自然知道何为规范标准,最重要的是面试的时候也是一份谈资!
因为优化项很多,所以我先举例出一些常用的,全部清单放在结尾。
第一部分:关键技术点深度解析 (Deep Dive)
针对下述清单中 10 个较难理解或较为重要的技术点,以下是详细的例子教学解析。
-
单次初始化 (Advanced Init Once)
问题:在 React 18+ 的 Strict Mode(开发环境)下,Effect 会执行两次,可能导致埋点 SDK 初始化两次或建立两个 WebSocket 连接。 解决方案:使用模块级变量或 useRef 做标记。
TypeScript
import { useEffect, useRef } from 'react';
export function App() {
const initialized = useRef(false);
useEffect(() => {
// 即使 Effect 跑两次,if 内部的代码只跑一次
if (!initialized.current) {
Analytics.init({ key: '...' }); // 全局只需一次的逻辑
initialized.current = true;
}
}, []);
}
-
Event Handler Refs
问题:useEffect 需要调用外部传入的函数(如 onMessage),但该函数经常变化,导致 Effect 频繁重启(断开重连)。 解决方案:用 Ref 存函数,打破依赖链。什么意思呢?就是有时候我们为了避免闭包陷阱需要使用到函数的最新值,但是如果用useState去存储的话会导致重复渲染,所以用useRef是个好方法。
TypeScript
function Chat({ onMessage }) {
// 1. 将最新的函数存入 Ref
const onMessageRef = useRef(onMessage);
useEffect(() => {
onMessageRef.current = onMessage;
});
useEffect(() => {
const socket = createSocket();
socket.on('msg', (data) => {
// 2. 在这里调用 Ref,不需要将 onMessage 加入依赖数组
onMessageRef.current(data);
});
// 3. 依赖项很干净,连接稳定
}, []);
}
-
交互逻辑移出 Effect (Rerender Event Logic)
原则:如果是用户点击引起的逻辑,直接写在 onClick 里,不要通过 State 触发 Effect。
- ❌ 反面教材:点击 -> 设置
isSubmitted(true)-> 触发 Effect -> 发请求 -> 设置isSubmitted(false)。 - ✅ 最佳实践:点击 ->
handleClick-> 发请求。
TypeScript
function Form() {
// ✅ Good: 逻辑清晰,少一次渲染
function handleSubmit() {
api.post('/submit');
}
return <button onClick={handleSubmit}>提交</button>;
}
-
派生状态 (Derived State)
原则:渲染时计算。减少 useState 和 useEffect 的滥用。
坏例子(冗余状态) :
function BadList({ items }) {
const [count, setCount] = useState(0);
// 浪费性能:当 items 变了,先渲染一次,运行 Effect,setCount,再渲染一次
useEffect(() => {
setCount(items.length);
}, [items]);
return <div>Count: {count}</div>;
}
好例子(直接计算) :
function GoodList({ items }) {
// 渲染时直接算。没有 Effect,没有额外的重渲染。
const count = items.length;
return <div>Count: {count}</div>;
}
-
LocalStorage 版本控制 (Schema Versioning)
问题:前端发版更新了数据结构,但用户本地存的是旧结构 JSON,导致解析报错白屏。 解决方案:存储时带上版本号,读取时校验。
TypeScript
const CURRENT_VER = 2;
function loadSettings() {
const raw = localStorage.getItem('cfg');
const data = JSON.parse(raw);
// 版本不匹配,丢弃旧数据,返回默认值
if (data?.version !== CURRENT_VER) {
localStorage.removeItem('cfg');
return DEFAULT_SETTINGS;
}
return data.payload;
}
-
被动监听器 (Passive: true)
含义:告诉浏览器“我承诺不调用 preventDefault()”。 作用:在移动端,浏览器不需要等待 JS 执行完就能立即滚动页面,极大提升滚动流畅度。
JavaScript
// 适用于 scroll, touchstart, touchmove
window.addEventListener('scroll', onScroll, { passive: true });
-
Next.js
after()(Server After)
场景:Serverless 函数在返回响应后会立即冻结。如果你想在 return 之后写日志,可能写不进去。 用法:使用 after() 延长 Serverless 函数的生命周期,直到任务完成。
TypeScript
import { after } from 'next/server';
export async function POST() {
const result = await doWork();
// 这里的代码会在响应返回给用户【之后】并在服务器关闭【之前】执行
after(() => {
logger.log('Task completed');
});
return Response.json(result);
}
-
跨请求缓存 (LRU Cache)
概念:Node.js 服务端是长期运行的。如果在全局变量存数据且不清理,内存会爆。 策略:LRU (Least Recently Used) 是一种“淘汰算法”,当缓存满了,优先删除“最近最少使用”的那条数据。 注意:只缓存公共数据(如菜单、配置),严禁缓存用户私有数据。
-
桶文件陷阱 (Barrel Files)
概念:桶文件指包含大量导出的 index.ts。 弊端:
- 变慢:开发环境启动时,Webpack 需要分析
index.ts里的所有导出,即使你只用了一个。 - Tree-shaking 失败:构建工具可能无法确定副作用,导致为了引入一个 Button 而打包了整个组件库。 对策:
import { Button } from './components/Button';(精确路径)。
-
推迟 Await (Async Defer Await)
核心:并行优于串行。
- ❌ 串行 (慢) :
- TypeScript
const user = await getUser(); // 等 1s
const posts = await getPosts(); // 再等 1s (总耗时 2s)
- ✅ 并行 (快) :
- TypeScript
const userPromise = getUser(); // 开始跑
const postsPromise = getPosts(); // 同时开始跑
// 等两个都好了再继续 (总耗时 ~1s)
const [user, posts] = await Promise.all([userPromise, postsPromise]);
第二部分:最佳实践完整清单 (The Full List)
本清单按 影响力 (Impact) 从高到低排序。在进行代码审查 (Code Review) 时,应优先关注高优先级的优化。
-
消除瀑布流加载 (Eliminating Waterfalls) - [关键/CRITICAL]
核心目标:减少网络请求等待时间,提升首屏速度。
- async-defer-await: 不要过早
await,只在真正需要数据时才等待,让 Promise 在后台预先运行。 - async-parallel: 独立的异步操作必须使用
Promise.all并行处理。 - async-dependencies: 优化请求依赖链,非依赖请求不应被阻塞。
- async-api-routes: 在 API 路由中尽早启动数据库查询等 Promise。
- async-suspense-boundaries: 使用 Suspense 流式传输内容,避免阻塞整个页面。
-
包体积优化 (Bundle Size Optimization) - [关键/CRITICAL]
核心目标:减小 JavaScript 体积,加快下载和解析速度。
- bundle-barrel-imports: 禁止从桶文件 (
index.js) 导入,必须直接从具体文件路径导入。 - bundle-dynamic-imports: 对重型组件(图表、地图)使用
next/dynamic或React.lazy懒加载。 - bundle-defer-third-party: 推迟加载非首屏必须的第三方脚本。
- bundle-preload-intent: 根据用户意图(如 Hover)预加载资源。
-
服务端性能 (Server-Side Performance) - [高/HIGH]
核心目标:优化 Next.js 服务端资源占用与响应速度。
- server-action-auth: Server Actions 必须包含权限校验。
- server-rsc-props-dupe: 避免在 RSC Props 中传递重复的大型数据对象。
- server-cross-request-cache: 使用 LRU 策略在请求间复用通用数据。
- server-rsc-serialization: 仅传递必要数据到客户端,减少序列化开销。
- server-request-dedup: 利用
React.cache对同一周期的请求去重。 - server-after: 使用
after()处理不阻塞响应的后台任务(日志、统计)。
-
客户端数据获取 (Client-Side Data Fetching) - [中高/MEDIUM-HIGH]
- client-swr-dedup: 使用 SWR/React Query 处理客户端请求去重。
- client-event-listeners: 确保全局事件监听器不重复添加,且在组件卸载时清理。
- client-passive-event-listeners: 滚动类事件必须开启
{ passive: true }。 - client-localstorage-schema: 对本地存储的数据进行版本控制。
-
重渲染优化 (Re-render Optimization) - [中/MEDIUM]
核心目标:减少 React 组件不必要的渲染次数。
- rerender-derived-state: 能计算得出的值,绝不设为 State。
- rerender-event-logic: 交互逻辑写在事件处理函数中,而非
useEffect。 - rerender-defer-read: 状态读取下沉到子组件。
- rerender-no-primitive-memo: 不要 Memo 简单类型(字符串/数字)。
- rerender-extract-default: 提取复杂的默认对象为常量。
- rerender-narrow-deps: 精简
useEffect依赖项。 - rerender-subscribe-derived: 只订阅状态的一部分。
- rerender-functional-updates: 使用
setState(prev => ...)减少依赖。
-
渲染性能 (Rendering Performance) - [中/MEDIUM]
- rendering-list-virtualization: 长列表使用虚拟滚动。
- rendering-image-optimization: 强制使用 Next.js
<Image>组件。 - rendering-font-optimization: 使用
next/font。
-
JavaScript 性能 (JavaScript Performance) - [低中/LOW-MEDIUM]
- js-heavy-computation: 繁重计算移至 Web Worker。
- js-timers: 严格管理定时器清理。
-
高级模式 (Advanced Patterns) - [低/LOW]
- advanced-event-handler-refs: 使用 Ref 存储最新回调以打破依赖链。
- advanced-init-once: 确保全局逻辑单次初始化。
- advanced-use-latest: 使用自定义 Hook 保持最新引用。
如果你对清单中的优化项有切身的实践经历,恳求评论区分享。