Skip to content

Vercel 团队 10 年 React 性能优化经验:10 大核心策略让性能提升 300% #406

@mqyqingfeng

Description

@mqyqingfeng

Vercel 最近发布了 React 最佳实践库,将十余年来积累的 React 和 Next.js 优化经验整合到了一个指南中。

其中一共包含8 个类别、40 多条规则

这些原则并不是纸上谈兵,而是 Vercel 团队在 10 余年从无数生产代码库中总结出的经验之谈。它们已经被无数成功案例验证,能切实改善用户体验和业务指标。

以下将是对你的 React 和 Next.js 项目影响最大的 10 大实践。

1. 将独立的异步操作并行

请求瀑布流是 React 应用性能的头号杀手。

每次顺序执行 await 都会增加网络延迟,消除它们可以带来最大的性能提升。

❌ 错误:

async function Page() {
  const user = await fetchUser();
  const posts = await fetchPosts();
  return <Dashboard user={user} posts={posts} />;
}

✅ 正确:

async function Page() {
  const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
  return <Dashboard user={user} posts={posts} />;
}

当处理多个数据源时,这个简单的改变可以将页面加载时间减少数百毫秒。

策略1:并行异步操作

2. 避免桶文件导入

从桶文件导入会强制打包程序解析整个库,即使你只需要其中一个组件。

这就像把整个衣柜都搬走,只为了穿一件衣服。

❌ 错误:

import { Check, X, Menu } from "lucide-react";

✅ 正确:

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 配置):

// next.config.js
module.exports = {
  experimental: {
    optimizePackageImports: ["lucide-react", "@mui/material"],
  },
};

// 然后保持简洁的导入方式
import { Check, X, Menu } from "lucide-react";

直接导入可将启动速度提高 15-70%,构建难度降低 28%,冷启动速度提高 40%,HMR 速度显著提高。

策略2:避免桶文件导入

3. 使用延迟状态初始化

当初始化状态需要进行耗时的计算时,将初始化程序包装在一个函数中,确保它只运行一次。

❌ 错误:

function Component() {
  const [config, setConfig] = useState(JSON.parse(localStorage.getItem("config")));
  return <div>{config.theme}</div>;
}

✅ 正确:

function Component() {
  const [config, setConfig] = useState(() => JSON.parse(localStorage.getItem("config")));
  return <div>{config.theme}</div>;
}

组件每次渲染都会从 localStorage 解析 JSON 配置,但其实它只需要在初始化的时候读取一次,将其封装在回调函数中可以消除这种浪费。

策略3:延迟状态初始化

4. 最小化 RSC 边界的数据传递

React 服务端/客户端边界会将所有对象属性序列化为字符串并嵌入到 HTML 响应中,这会直接影响页面大小和加载时间。

❌ 错误:

async function Page() {
  const user = await fetchUser(); // 50 fields
  return <Profile user={user} />;
}

("use client");
function Profile({ user }) {
  return <div>{user.name}</div>; // uses 1 field
}

✅ 正确:

async function Page() {
  const user = await fetchUser();
  return <Profile name={user.name} />;
}

("use client");
function Profile({ name }) {
  return <div>{name}</div>;
}

只传递客户端组件实际需要的数据。

策略4:最小化RSC边界

5. 动态导入大型组件

仅在功能激活时加载大型库,减少初始包体积。

❌ 错误:

import { AnimationPlayer } from "./heavy-animation-lib";

function Component() {
  const [enabled, setEnabled] = useState(false);
  return enabled ? <AnimationPlayer /> : null;
}

✅ 正确:

function AnimationPlayer({ enabled, setEnabled }) {
  const [frames, setFrames] = useState(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 可以防止将此模块打包用于 SSR,优化服务端包体积和构建速度。

策略5:动态导入组件

6. 延迟加载第三方脚本

分析和跟踪脚本不要阻塞用户交互。

❌ 错误:

export default function RootLayout({ children }) {
  useEffect(() => {
    initAnalytics();
  }, []);

  return (
    <html>
      <body>{children}</body>
    </html>
  );
}

✅ 正确:

import { Analytics } from "@vercel/analytics/react";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Analytics />
      </body>
    </html>
  );
}

在水合后加载分析脚本,优先处理交互内容。

策略6:延迟加载脚本

7. 使用 React.cache() 进行请求去重

防止服务端在同一渲染周期内重复请求。

❌ 错误:

async function Sidebar() {
  const user = await fetchUser();
  return <div>{user.name}</div>;
}

async function Header() {
  const user = await fetchUser(); // 重复请求
  return <nav>{user.email}</nav>;
}

✅ 正确:

import { cache } from "react";

const getUser = cache(async () => {
  return await fetchUser();
});

async function Sidebar() {
  const user = await getUser();
  return <div>{user.name}</div>;
}

async function Header() {
  const user = await getUser(); // 已缓存,无重复请求
  return <nav>{user.email}</nav>;
}

策略7-8:缓存去重

8. 实现跨请求数据的 LRU 缓存

React.cache() 仅在单个请求内有效,因此对于跨连续请求共享的数据,使用 LRU 缓存。

❌ 错误:

import { LRUCache } from "lru-cache";

const cache = new LRUCache({
  max: 1000,
  ttl: 5 * 60 * 1000, // 5 分钟
});

export async function getUser(id) {
  const cached = cache.get(id);
  if (cached) return cached;

  const user = await db.user.findUnique({ where: { id } });
  cache.set(id, user);
  return user;
}

这在 Vercel 的 Fluid Compute 中特别有效,多个并发请求共享同一个函数实例。

9. 通过组件组合实现并行化

React 服务端组件在树状结构中按顺序执行,因此需要使用组合对组件树进行重构以实现并行化数据获取:

❌ 错误:

async function Page() {
  const data = await fetchPageData();
  return (
    <>
      <Header />
      <Sidebar data={data} />
    </>
  );
}

✅ 正确:

async function Page() {
  return (
    <>
      <Header />
      <Sidebar />
    </>
  );
}

async function Sidebar() {
  const data = await fetchPageData();
  return <div>{data.content}</div>;
}

这样一来,页眉和侧边栏就可以并行获取数据了。

10. 使用 SWR 进行客户端请求去重

当客户端上的多个组件请求相同的数据时,SWR 会自动对请求进行去重。

❌ 错误:

function UserProfile() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch("/api/user")
      .then((r) => r.json())
      .then(setUser);
  }, []);

  return <div>{user?.name}</div>;
}

function UserAvatar() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch("/api/user")
      .then((r) => r.json())
      .then(setUser);
  }, []);

  return <img src={user?.avatar} />;
}

✅ 正确:

import useSWR from "swr";

const fetcher = (url) => fetch(url).then((r) => r.json());

function UserProfile() {
  const { data: user } = useSWR("/api/user", fetcher);
  return <div>{user?.name}</div>;
}

function UserAvatar() {
  const { data: user } = useSWR("/api/user", fetcher);
  return <img src={user?.avatar} />;
}

SWR 只发出一个请求,并将结果在两个组件之间共享。

11. 总结

这些最佳实践的美妙之处在于:它们不是复杂的架构变更。大多数都是简单的代码修改,却能产生显著的性能改进。

一个 600ms 的瀑布等待时间,会影响每一位用户,直到被修复。

一个桶文件导入造成的包膨胀,会减慢每一次构建和每一次页面加载

所以越早采用这些实践,就能避免积累越来越多的性能债务。

总结:立即行动

现在开始应用这些技巧,让你的 React 应用快如闪电吧!

我是冴羽,10 年笔耕不辍,专注前端领域,更新了 10+ 系列、300+ 篇原创技术文章,翻译过 Svelte、Solid.js、TypeScript 文档,著有小册《Next.js 开发指南》、《Svelte 开发指南》、《Astro 实战指南》。

欢迎围观我的“网页版朋友圈”,关注我的公众号:冴羽(或搜索 yayujs),每天分享前端知识、AI 干货。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions