React & Next.js官方教你的编程技巧!

26 阅读7分钟

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 个较难理解或较为重要的技术点,以下是详细的例子教学解析。

  1. 单次初始化 (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;
    }
  }, []);
}
  1. 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. 依赖项很干净,连接稳定
  }, []); 
}
  1. 交互逻辑移出 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>;
}
  1. 派生状态 (Derived State)

原则渲染时计算。减少 useStateuseEffect 的滥用。

坏例子(冗余状态)

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>;
}
  1. 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;
}
  1. 被动监听器 (Passive: true)

含义:告诉浏览器“我承诺不调用 preventDefault()”。 作用:在移动端,浏览器不需要等待 JS 执行完就能立即滚动页面,极大提升滚动流畅度。

JavaScript

// 适用于 scroll, touchstart, touchmove
window.addEventListener('scroll', onScroll, { passive: true });
  1. 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);
}
  1. 跨请求缓存 (LRU Cache)

概念:Node.js 服务端是长期运行的。如果在全局变量存数据且不清理,内存会爆。 策略:LRU (Least Recently Used) 是一种“淘汰算法”,当缓存满了,优先删除“最近最少使用”的那条数据。 注意:只缓存公共数据(如菜单、配置),严禁缓存用户私有数据。

  1. 桶文件陷阱 (Barrel Files)

概念:桶文件指包含大量导出的 index.ts弊端

  1. 变慢:开发环境启动时,Webpack 需要分析 index.ts 里的所有导出,即使你只用了一个。
  2. Tree-shaking 失败:构建工具可能无法确定副作用,导致为了引入一个 Button 而打包了整个组件库。 对策import { Button } from './components/Button'; (精确路径)。
  1. 推迟 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) 时,应优先关注高优先级的优化。

  1. 消除瀑布流加载 (Eliminating Waterfalls) - [关键/CRITICAL]

核心目标:减少网络请求等待时间,提升首屏速度。

  • async-defer-await: 不要过早 await,只在真正需要数据时才等待,让 Promise 在后台预先运行。
  • async-parallel: 独立的异步操作必须使用 Promise.all 并行处理。
  • async-dependencies: 优化请求依赖链,非依赖请求不应被阻塞。
  • async-api-routes: 在 API 路由中尽早启动数据库查询等 Promise。
  • async-suspense-boundaries: 使用 Suspense 流式传输内容,避免阻塞整个页面。
  1. 包体积优化 (Bundle Size Optimization) - [关键/CRITICAL]

核心目标:减小 JavaScript 体积,加快下载和解析速度。

  • bundle-barrel-imports: 禁止从桶文件 (index.js) 导入,必须直接从具体文件路径导入。
  • bundle-dynamic-imports: 对重型组件(图表、地图)使用 next/dynamicReact.lazy 懒加载。
  • bundle-defer-third-party: 推迟加载非首屏必须的第三方脚本。
  • bundle-preload-intent: 根据用户意图(如 Hover)预加载资源。
  1. 服务端性能 (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() 处理不阻塞响应的后台任务(日志、统计)。
  1. 客户端数据获取 (Client-Side Data Fetching) - [中高/MEDIUM-HIGH]

  • client-swr-dedup: 使用 SWR/React Query 处理客户端请求去重。
  • client-event-listeners: 确保全局事件监听器不重复添加,且在组件卸载时清理。
  • client-passive-event-listeners: 滚动类事件必须开启 { passive: true }
  • client-localstorage-schema: 对本地存储的数据进行版本控制。
  1. 重渲染优化 (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 => ...) 减少依赖。
  1. 渲染性能 (Rendering Performance) - [中/MEDIUM]

  • rendering-list-virtualization: 长列表使用虚拟滚动。
  • rendering-image-optimization: 强制使用 Next.js <Image> 组件。
  • rendering-font-optimization: 使用 next/font
  1. JavaScript 性能 (JavaScript Performance) - [低中/LOW-MEDIUM]

  • js-heavy-computation: 繁重计算移至 Web Worker。
  • js-timers: 严格管理定时器清理。
  1. 高级模式 (Advanced Patterns) - [低/LOW]

  • advanced-event-handler-refs: 使用 Ref 存储最新回调以打破依赖链。
  • advanced-init-once: 确保全局逻辑单次初始化。
  • advanced-use-latest: 使用自定义 Hook 保持最新引用。

如果你对清单中的优化项有切身的实践经历,恳求评论区分享。