前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
原理篇
面经篇
AI 面试
自检篇
每日一题
  • 综合
    • 综合题型
    • 其他问题
    • 设计模式
    • 思维导图
    • 学习路线
  • 前端基础
    • HTTP
    • 浏览器
    • 计算机基础
  • 进阶学习
    • NPM工作流
    • Docker
    • Canvas
    • Node学习指南
    • 前端综合文章
  • 其他
    • Handbook
    • 职场话题
    • CSS可视化
小程序题库
公众号动态
博客动态
开发者导航
基础篇
进阶篇
高频篇
精选篇
手写篇
原理篇
面经篇
AI 面试
自检篇
每日一题
  • 综合
    • 综合题型
    • 其他问题
    • 设计模式
    • 思维导图
    • 学习路线
  • 前端基础
    • HTTP
    • 浏览器
    • 计算机基础
  • 进阶学习
    • NPM工作流
    • Docker
    • Canvas
    • Node学习指南
    • 前端综合文章
  • 其他
    • Handbook
    • 职场话题
    • CSS可视化
小程序题库
公众号动态
博客动态
开发者导航

useMemo救不了你的React性能-精读Vercel官方性能优化70条军规

首页2026-07-26 15:42:18Front-End
ReactNext.js性能优化

文章首发于: https://feinterview.poetries.top/blog/vercel-react-best-practices

你是否也有这样的经历:页面卡了,第一反应是打开组件加一圈 useMemo、useCallback、memo(),加完用 Profiler 一测,首屏该慢还是慢。问题出在哪?出在优化的优先级排反了。

Vercel 工程团队在 2026 年初开源了一份 React Best Practices(vercel-react-best-practices),把内部沉淀的 70 条 React / Next.js 性能规则整理成机器可读的 skill,喂给 AI 编码工具做代码生成和 review 的依据,人读同样受益。这份清单最值钱的不是某条具体技巧,而是它给所有优化手段按影响力排了序:消灭请求瀑布流和砍包体积是 CRITICAL,大家最爱写的 re-render 优化只排在 MEDIUM,useMemo 的部分用法甚至被列为反模式。

我把整份规则读完,这篇文章按 ROI 从高到低精讲其中最有实战价值的部分,附原版代码对比和使用边界。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 这份清单的 8 大类规则与优先级全景
  • CRITICAL 级第一名:消灭请求瀑布流的 6 种手法
  • CRITICAL 级第二名:包体积优化,barrel import 一条就能省 200-800ms
  • HIGH 级:服务端性能,RSC 时代最容易踩的并发与序列化坑
  • MEDIUM 级:re-render 优化的正确姿势,以及哪些 useMemo 应该删掉
  • 低优先级但常考的 JavaScript 微优化速查表
  • 怎么把这份规则接入你的 AI 编码工作流

# 一、先看全景 70 条规则怎么分层

清单按影响力分 8 类,优先级从上到下递减:

优先级 类别 影响力 规则前缀
1 消灭瀑布流 CRITICAL async-
2 包体积优化 CRITICAL bundle-
3 服务端性能 HIGH server-
4 客户端数据获取 MEDIUM-HIGH client-
5 re-render 优化 MEDIUM rerender-
6 渲染性能 MEDIUM rendering-
7 JavaScript 微优化 LOW-MEDIUM js-
8 高级模式 LOW advanced-

这个排序背后的逻辑很硬:一次串行的网络往返是几十到几百毫秒,一次多余的 re-render 通常不到一毫秒。瀑布流每多一层就多付一次完整网络延迟,而你精心 memo 掉的那次渲染,用户根本感知不到。所以官方原话是「Waterfalls are the #1 performance killer」——先治网络和包体积,再谈渲染。

React 性能优化 70 条军规优先级金字塔:消灭瀑布流和包体积是 CRITICAL 级,越往上收益越大,先治网络和包体积再谈渲染

下面按这个顺序逐层拆。

# 二、消灭瀑布流 每一个多余的await都在烧钱

# 2.1 Promise.all 是及格线

最基础的一条:没有依赖关系的异步操作绝不串行。

这段是反面示例,三个请求串成三次往返:

const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
@前端进阶之旅: 代码已经复制到剪贴板

改成并行后只付一次往返的时间,官方给的量化收益是 2-10 倍:

const [user, posts, comments] = await Promise.all([
  fetchUser(),
  fetchPosts(),
  fetchComments()
])
@前端进阶之旅: 代码已经复制到剪贴板

串行瀑布流三次往返 vs Promise.all 并行一次往返的时间线对比,官方量化收益 2-10 倍提速

# 2.2 部分依赖场景 先启动再await

真正拉开差距的是「半依赖」场景:profile 依赖 user,但 config 谁都不依赖。多数人会写成两段 Promise.all,结果 profile 白白等了 config。正确做法是先创建所有 promise,最后统一 await:

const userPromise = fetchUser()
const profilePromise = userPromise.then(user => fetchProfile(user.id))

const [user, config, profile] = await Promise.all([
  userPromise,
  fetchConfig(),
  profilePromise
])
@前端进阶之旅: 代码已经复制到剪贴板

这样 config 和整条 user → profile 链是并行的,每个任务都在依赖就绪的第一时间启动。同样的思路用在 API 路由里:auth() 和 fetchConfig() 互不依赖,就先把两个 promise 都启动,再按需 await,不要让 config 排在 auth 后面白等一轮。

# 2.3 defer await 把等待推进真正用到的分支

另一条容易忽视的规则:await 应该出现在用到结果的分支里,而不是函数开头。比如权限校验,如果 resource 不存在就直接返回 404,那 fetchPermissions 这次请求就完全不该发生。把 await 后移,走「资源不存在」快路径的请求一次网络开销都不用付。同理,flag && cheapCondition 这种组合条件,先判本地的 cheapCondition,为 false 就压根不去 await 远端的 flag。

# 2.4 Suspense 让骨架先上屏

页面级组件里 await 一把梭,整个布局都会被这次取数拖住。官方建议把取数下沉到子组件、外层包 Suspense:

function Page() {
  return (
    <div>
      <Sidebar />
      <Suspense fallback={<Skeleton />}>
        <DataDisplay />
      </Suspense>
      <Footer />
    </div>
  )
}

async function DataDisplay() {
  const data = await fetchData() // 只阻塞这一块
  return <div>{data.content}</div>
}
@前端进阶之旅: 代码已经复制到剪贴板

Sidebar 和 Footer 立即上屏,数据流式补进中间区域。注意清单里也写明了不适用场景:影响布局定位的关键数据、首屏 SEO 内容、以及快到不值得付出布局抖动代价的小请求。关于 Suspense 与并发渲染的底层机制,可以搭配我之前写的 React 18 并发机制详解 一起看。

# 三、包体积 一行import偷走你200-800ms

bundle- 类里最触目惊心的是 barrel import 这条。所谓 barrel 文件就是库入口那个 export * from ... 的 index.js,流行的图标库、组件库入口动辄上万个 re-export:

import { Check, X, Menu } from 'lucide-react'
// 实际加载 1583 个模块,dev 慢 ~2.8s,冷启动多付 200-800ms

import { Button, TextField } from '@mui/material'
// 实际加载 2225 个模块
@前端进阶之旅: 代码已经复制到剪贴板

你以为 tree-shaking 会救你,但清单里点破了:库被标为 external 时打包器根本不做优化;要 tree-shake 就得全量分析模块图,build 时间又爆了。解法有两个:Next.js 13.5+ 直接靠内置的 optimizePackageImports,代码不用改;其他项目走子路径直连 import Button from '@mui/material/Button'。官方给的收益是 dev 启动快 15-70%、构建快 28%、冷启动快 40%。受影响的常见库包括 lucide-react、react-icons、lodash、date-fns、rxjs 等——检查一下你的项目,大概率中招。

barrel import 一行拖进 1583 个模块浪费 200-800ms,直连导入只加载所需、更快冷启动

这一类还有三条高价值规则,逻辑一以贯之——首包只留首屏必需的东西:

  • 重组件动态加载:Monaco、图表库这种 300KB 起步的大件用 next/dynamic 按需拉,直接改善 TTI 和 LCP
  • 第三方库延后:analytics、错误上报不阻塞交互,ssr: false 等 hydration 完了再加载
  • 按用户意图预加载:按钮 onMouseEnter / onFocus 时 void import('./monaco-editor'),等用户真点开时模块已经在缓存里,体感零延迟

另外注意动态导入要保持路径静态可分析:import(SOME_MAP[key]) 这种写法打包器猜不出范围,只能把可能的文件全打进来;改成显式的 { home: () => import('./pages/home') } 映射表,产物立刻收窄。

# 四、服务端性能 RSC时代的新坑

server- 类 10 条规则里有三条是 Next.js App Router 项目几乎必踩的坑。

第一坑:Server Action 忘了鉴权。 "use server" 函数会被编译成公开 endpoint,middleware 和页面级守卫拦不住直接调用。所以每个 action 内部都必须自带 verifySession() + 权限校验,这条被标为 CRITICAL——它不是性能问题,是安全事故。

第二坑:模块级变量存请求数据。 服务端并发渲染时模块作用域是进程共享的,let currentUser = null 这种写法会让 A 用户的数据串到 B 用户的响应里。请求级数据一律走 props 或 React.cache(),模块级只放不可变的静态资源。

第三坑:RSC 边界序列化超载。 Server/Client 边界上所有 props 都会被序列化进 HTML,50 个字段的 user 对象只用一个 name 也全量传输。清单的建议是只传用到的字段,并且不要在服务端做 .filter() / .toSorted() 再和原数组一起传——序列化按引用去重,新引用意味着同一份数据传两遍。

取数方面还有两条组合拳值得记住。React.cache() 做请求内去重(数据库查询、鉴权这类非 fetch 的异步操作收益最大,fetch 本身 Next.js 已自动去重);跨请求就上 LRU 缓存。嵌套取数则要把依赖链塞进每个 item 自己的 promise 里:

// 反例:一个慢 chat 卡住全部 author 请求
const chats = await Promise.all(chatIds.map(id => getChat(id)))
const authors = await Promise.all(chats.map(c => getUser(c.author)))

// 正解:每个 item 独立成链,互不阻塞
const authors = await Promise.all(
  chatIds.map(id => getChat(id).then(chat => getUser(chat.author)))
)
@前端进阶之旅: 代码已经复制到剪贴板

最后是 after():日志、埋点、通知这类副作用放进 after(() => ...),响应先发出去,副作用在后台跑。这些能力在 Next.js 15 之后已经稳定,相关背景我在 Next.js 15 如何让 React 应用更快 里写过。

# 五、re-render优化 一半的useMemo本该删掉

到了大家最熟的领域,清单反而在泼冷水。15 条 rerender- 规则里,我认为最值得内化的是这四条。

第二条:简单表达式别包 useMemo。 这是清单里少见的「反 memo」规则:

# 总结

# 参考

fe
  • 一、先看全景 70 条规则怎么分层
  • 二、消灭瀑布流 每一个多余的await都在烧钱
    • 2.1 Promise.all 是及格线
    • 2.2 部分依赖场景 先启动再await
    • 2.3 defer await 把等待推进真正用到的分支
    • 2.4 Suspense 让骨架先上屏
  • 三、包体积 一行import偷走你200-800ms
  • 四、服务端性能 RSC时代的新坑
  • 五、re-render优化 一半的useMemo本该删掉
  • 六、渲染与JS微优化 速查表
  • 七、把这份清单接进AI编码工作流
  • 总结
  • 参考

WireGuard + mitmproxy 全局抓 iOS App HTTPS 明文实战从隧道到证书一次打通 →