Skip to content

第15章 性能优化实践

本章将系统讲解React 19全栈应用的性能优化策略,从服务端到客户端,从理论到实践,帮助你构建高性能的React应用。

React 19作为全栈框架,性能优化不再局限于客户端。服务端渲染、React Server Components、流式传输等新特性为性能优化带来了全新的思路。如何在服务端和客户端之间找到最佳平衡?如何充分利用RSC减少客户端JavaScript?如何优化数据获取和渲染流程?

传统的客户端优化技巧(如React.memo()、代码分割)依然重要,但在全栈架构下,我们需要更全面的优化视角。服务端优先渲染、合理的组件边界划分、智能的缓存策略,这些都是构建高性能React应用的关键。

本章将从全栈视角出发,系统讲解React 19的性能优化原则与实践。我们将深入RSC的性能优化技巧,探讨客户端优化的最佳实践,并通过实际案例展示如何定位和解决性能瓶颈。


15.1 全栈React性能优化原则

在React 19的全栈架构下,性能优化需要从整体视角考虑服务端和客户端的协作。

15.1.1 服务端优先渲染

为什么服务端优先?

React 19的核心理念是"服务端优先"(Server-First)。这不是说完全放弃客户端渲染,而是将计算密集型任务尽可能放在服务端执行。

服务端的优势

  1. 更强的计算能力

    • 服务器通常比用户设备性能更强
    • 可以执行复杂的数据处理和计算
    • 不受用户设备性能限制
  2. 更快的数据访问

    • 直接访问数据库,无需网络往返
    • 可以并行获取多个数据源
    • 减少客户端的网络请求
  3. 更小的客户端Bundle

    • 服务端组件的代码不会发送到客户端
    • 减少JavaScript下载和解析时间
    • 改善首屏加载性能

服务端优先的实践

jsx
// ❌ 不好的做法:在客户端获取数据
'use client';
import { useState, useEffect } from 'react';

export default function ProductList() {
  const [products, setProducts] = useState([]);
  
  useEffect(() => {
    // 客户端发起网络请求
    fetch('/api/products')
      .then(res => res.json())
      .then(setProducts);
  }, []);
  
  return (
    <div>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

// ✅ 好的做法:在服务端获取数据
// 默认是Server Component,无需'use client'
import { db } from '@/lib/database';

export default async function ProductList() {
  // 在服务端直接查询数据库
  const products = await db.products.findMany();
  
  return (
    <div>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

性能对比

方案首屏时间JavaScript大小网络请求
客户端获取慢(需等待JS执行)大(包含fetch逻辑)2次(HTML + API)
服务端获取快(HTML已包含数据)小(无fetch代码)1次(HTML)

15.1.2 减少客户端JavaScript

JavaScript是性能瓶颈

在现代Web应用中,JavaScript往往是最大的性能瓶颈:

  1. 下载时间:大型Bundle需要更长的下载时间
  2. 解析时间:JavaScript引擎需要解析代码
  3. 执行时间:代码执行占用主线程
  4. 内存占用:大量JavaScript消耗设备内存

React Server Components的优势

RSC的核心价值之一就是减少客户端JavaScript:

jsx
// 文件:app/dashboard/page.js
// 这是一个Server Component
import { AnalyticsChart } from '@/components/AnalyticsChart';
import { UserTable } from '@/components/UserTable';
import { RevenueCard } from '@/components/RevenueCard';

export default async function Dashboard() {
  const [analytics, users, revenue] = await Promise.all([
    fetchAnalytics(),
    fetchUsers(),
    fetchRevenue(),
  ]);
  
  return (
    <div className="dashboard">
      <RevenueCard data={revenue} />
      <AnalyticsChart data={analytics} />
      <UserTable users={users} />
    </div>
  );
}

Bundle大小对比

假设每个组件的依赖如下:

  • AnalyticsChart: 使用Chart.js (50KB)
  • UserTable: 使用date-fns (20KB)
  • RevenueCard: 使用numeral.js (10KB)
方案客户端JavaScript
全部Client Component80KB + React代码
全部Server Component0KB(仅React运行时)

何时使用Client Component

并非所有组件都应该是Server Component。以下情况需要使用Client Component:

  1. 需要交互性:点击、输入、拖拽等
  2. 使用浏览器API:localStorage、geolocation等
  3. 使用React Hooks:useState、useEffect等
  4. 需要实时更新:WebSocket、轮询等
jsx
// 文件:components/InteractiveChart.js
'use client';
import { useState } from 'react';
import { LineChart } from 'recharts';

export function InteractiveChart({ data }) {
  const [selectedRange, setSelectedRange] = useState('7d');
  
  return (
    <div>
      <select value={selectedRange} onChange={e => setSelectedRange(e.target.value)}>
        <option value="7d">Last 7 days</option>
        <option value="30d">Last 30 days</option>
      </select>
      <LineChart data={data[selectedRange]} />
    </div>
  );
}

15.1.3 合理划分组件边界

组件边界的重要性

在全栈React中,组件边界的划分直接影响性能:

  1. Server/Client边界:决定哪些代码发送到客户端
  2. Suspense边界:决定加载状态的粒度
  3. 错误边界:决定错误处理的范围

Server/Client边界划分原则

jsx
// ❌ 不好的做法:整个页面都是Client Component
'use client';
import { useState } from 'react';

export default function ProductPage({ productId }) {
  const [quantity, setQuantity] = useState(1);
  const product = useProduct(productId); // 客户端获取数据
  const reviews = useReviews(productId); // 客户端获取数据
  
  return (
    <div>
      <ProductInfo product={product} />
      <QuantitySelector quantity={quantity} onChange={setQuantity} />
      <Reviews reviews={reviews} />
    </div>
  );
}
jsx
// ✅ 好的做法:最小化Client Component范围
// 文件:app/product/[id]/page.js
import { db } from '@/lib/database';
import { QuantitySelector } from './QuantitySelector';

export default async function ProductPage({ params }) {
  // 在服务端并行获取数据
  const [product, reviews] = await Promise.all([
    db.products.findUnique({ where: { id: params.id } }),
    db.reviews.findMany({ where: { productId: params.id } }),
  ]);
  
  return (
    <div>
      {/* Server Component */}
      <ProductInfo product={product} />
      
      {/* 只有需要交互的部分是Client Component */}
      <QuantitySelector />
      
      {/* Server Component */}
      <Reviews reviews={reviews} />
    </div>
  );
}

// 文件:app/product/[id]/QuantitySelector.js
'use client';
import { useState } from 'react';

export function QuantitySelector() {
  const [quantity, setQuantity] = useState(1);
  
  return (
    <div>
      <button onClick={() => setQuantity(q => q - 1)}>-</button>
      <span>{quantity}</span>
      <button onClick={() => setQuantity(q => q + 1)}>+</button>
    </div>
  );
}

组件边界划分的黄金法则

  1. 默认使用Server Component

    • 除非明确需要交互性,否则使用Server Component
    • 可以随时将Server Component改为Client Component
  2. 将Client Component推到叶子节点

    • 尽可能将'use client'放在组件树的底部
    • 避免在顶层使用'use client'
  3. 使用组合模式传递Server Component

    jsx
    // Client Component可以接收Server Component作为children
    'use client';
    export function ClientWrapper({ children }) {
      const [isOpen, setIsOpen] = useState(false);
      return <div onClick={() => setIsOpen(!isOpen)}>{children}</div>;
    }
    
    // 使用
    <ClientWrapper>
      <ServerComponent /> {/* 这仍然是Server Component */}
    </ClientWrapper>

15.1.4 数据获取优化

并行数据获取

避免瀑布式请求,尽可能并行获取数据:

jsx
// ❌ 不好的做法:串行获取
export default async function Dashboard() {
  const user = await fetchUser();
  const posts = await fetchPosts(user.id); // 等待user完成
  const comments = await fetchComments(posts[0].id); // 等待posts完成
  
  return <div>...</div>;
}

// ✅ 好的做法:并行获取
export default async function Dashboard() {
  const userPromise = fetchUser();
  const postsPromise = fetchPosts();
  const commentsPromise = fetchComments();
  
  // 并行等待所有请求
  const [user, posts, comments] = await Promise.all([
    userPromise,
    postsPromise,
    commentsPromise,
  ]);
  
  return <div>...</div>;
}

预加载数据

使用React的preload模式提前开始数据获取:

jsx
// 文件:lib/data.js
import { cache } from 'react';

export const getUser = cache(async (id) => {
  return await db.user.findUnique({ where: { id } });
});

// 预加载函数
export function preloadUser(id) {
  void getUser(id); // 启动请求但不等待
}

// 文件:app/user/[id]/page.js
import { getUser, preloadUser } from '@/lib/data';

export default async function UserPage({ params }) {
  // 数据可能已经在缓存中
  const user = await getUser(params.id);
  return <UserProfile user={user} />;
}

// 在Link中预加载
<Link 
  href={`/user/${userId}`}
  onMouseEnter={() => preloadUser(userId)}
>
  View Profile
</Link>

15.1.5 流式渲染优化

使用Suspense分段渲染

将页面分成多个Suspense边界,实现渐进式渲染:

jsx
import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <div>
      {/* 快速渲染的部分 */}
      <Header />
      
      {/* 慢速数据独立加载 */}
      <Suspense fallback={<SkeletonChart />}>
        <AnalyticsChart />
      </Suspense>
      
      <Suspense fallback={<SkeletonTable />}>
        <UserTable />
      </Suspense>
      
      {/* 静态内容 */}
      <Footer />
    </div>
  );
}

Suspense边界的粒度

性能收益

方案TTFBFCPLCP
无Suspense(等待所有数据)100ms2000ms2000ms
使用Suspense(分段渲染)100ms300ms800ms

15.2 RSC性能优化

React Server Components是React 19性能优化的核心。深入理解RSC的工作原理,才能充分发挥其性能优势。

15.2.1 数据获取优化

避免N+1查询问题

在服务端组件中,最常见的性能问题是N+1查询:

jsx
// ❌ 不好的做法:N+1查询
export default async function PostList() {
  const posts = await db.posts.findMany();
  
  return (
    <div>
      {posts.map(post => (
        // 每个Post组件都会发起一次数据库查询
        <Post key={post.id} postId={post.id} />
      ))}
    </div>
  );
}

async function Post({ postId }) {
  const post = await db.posts.findUnique({ where: { id: postId } });
  const author = await db.users.findUnique({ where: { id: post.authorId } }); // N次查询
  
  return <div>{post.title} by {author.name}</div>;
}
jsx
// ✅ 好的做法:使用JOIN或预加载
export default async function PostList() {
  // 一次查询获取所有数据
  const posts = await db.posts.findMany({
    include: {
      author: true, // JOIN查询
    },
  });
  
  return (
    <div>
      {posts.map(post => (
        <Post key={post.id} post={post} />
      ))}
    </div>
  );
}

function Post({ post }) {
  // 数据已经包含author,无需额外查询
  return <div>{post.title} by {post.author.name}</div>;
}

使用DataLoader模式

对于复杂的数据依赖,可以使用DataLoader模式批量获取数据:

javascript
// 文件:lib/dataloader.js
import DataLoader from 'dataloader';
import { db } from './database';

// 批量加载用户
const userLoader = new DataLoader(async (userIds) => {
  const users = await db.users.findMany({
    where: { id: { in: userIds } },
  });
  
  // 返回与userIds顺序一致的结果
  const userMap = new Map(users.map(u => [u.id, u]));
  return userIds.map(id => userMap.get(id));
});

export function getUser(id) {
  return userLoader.load(id);
}
jsx
// 使用DataLoader
import { getUser } from '@/lib/dataloader';

async function Post({ post }) {
  // 多个Post组件的getUser调用会被批量处理
  const author = await getUser(post.authorId);
  
  return <div>{post.title} by {author.name}</div>;
}

性能对比

方案数据库查询次数响应时间
N+1查询1 + N500ms(100个post)
JOIN查询150ms
DataLoader2(posts + 批量users)60ms

15.2.2 流式渲染优化

合理设置Suspense边界

Suspense边界的位置直接影响用户体验:

jsx
// ❌ 不好的做法:整个页面一个Suspense
export default function Page() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Dashboard />
    </Suspense>
  );
}

async function Dashboard() {
  // 等待所有数据
  const [user, posts, analytics] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchAnalytics(), // 这个很慢
  ]);
  
  return (
    <div>
      <UserInfo user={user} />
      <PostList posts={posts} />
      <Analytics data={analytics} />
    </div>
  );
}
jsx
// ✅ 好的做法:细粒度Suspense边界
export default function Page() {
  return (
    <div>
      {/* 快速数据独立渲染 */}
      <Suspense fallback={<UserSkeleton />}>
        <UserInfo />
      </Suspense>
      
      <Suspense fallback={<PostsSkeleton />}>
        <PostList />
      </Suspense>
      
      {/* 慢速数据不阻塞其他内容 */}
      <Suspense fallback={<AnalyticsSkeleton />}>
        <Analytics />
      </Suspense>
    </div>
  );
}

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

async function PostList() {
  const posts = await fetchPosts(); // 中速
  return <div>{posts.map(p => <Post key={p.id} post={p} />)}</div>;
}

async function Analytics() {
  const data = await fetchAnalytics(); // 慢速
  return <Chart data={data} />;
}

流式渲染时序图

优先级控制

使用priority属性控制Suspense边界的优先级(实验性特性):

jsx
<Suspense fallback={<Skeleton />} priority="high">
  <CriticalContent />
</Suspense>

<Suspense fallback={<Skeleton />} priority="low">
  <NonCriticalContent />
</Suspense>

15.2.3 缓存策略

React Cache API

使用cache函数避免重复的数据获取:

javascript
// 文件:lib/data.js
import { cache } from 'react';
import { db } from './database';

// 在同一次渲染中,相同参数的调用会返回缓存结果
export const getUser = cache(async (id) => {
  console.log('Fetching user:', id);
  return await db.users.findUnique({ where: { id } });
});

export const getPosts = cache(async (userId) => {
  console.log('Fetching posts for user:', userId);
  return await db.posts.findMany({ where: { authorId: userId } });
});
jsx
// 多个组件调用getUser(1)只会执行一次数据库查询
import { getUser } from '@/lib/data';

async function UserProfile({ userId }) {
  const user = await getUser(userId); // 第1次调用
  return <div>{user.name}</div>;
}

async function UserPosts({ userId }) {
  const user = await getUser(userId); // 使用缓存,不会再次查询
  const posts = await getPosts(userId);
  return <div>{user.name}的文章: {posts.length}</div>;
}

Next.js的fetch缓存

Next.js扩展了fetchAPI,提供了强大的缓存能力:

javascript
// 默认缓存,直到手动失效
const data = await fetch('https://api.example.com/data');

// 每10秒重新验证
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 10 }
});

// 不缓存,每次都重新获取
const data = await fetch('https://api.example.com/data', {
  cache: 'no-store'
});

// 使用标签,方便批量失效
const data = await fetch('https://api.example.com/data', {
  next: { tags: ['products'] }
});

// 在Server Action中失效缓存
import { revalidateTag } from 'next/cache';

export async function updateProduct() {
  await db.products.update(/*...*/);
  revalidateTag('products'); // 失效所有带'products'标签的缓存
}

缓存层级

15.2.4 组件粒度优化

避免过度细分

虽然Server Component没有客户端Bundle成本,但过度细分会增加网络开销:

jsx
// ❌ 不好的做法:过度细分
async function UserCard({ userId }) {
  return (
    <div>
      <UserAvatar userId={userId} />
      <UserName userId={userId} />
      <UserBio userId={userId} />
      <UserStats userId={userId} />
    </div>
  );
}

// 每个子组件都独立获取用户数据,造成重复查询
async function UserAvatar({ userId }) {
  const user = await getUser(userId);
  return <img src={user.avatar} />;
}

async function UserName({ userId }) {
  const user = await getUser(userId);
  return <h2>{user.name}</h2>;
}
jsx
// ✅ 好的做法:合理粒度
async function UserCard({ userId }) {
  // 一次获取所有需要的数据
  const user = await getUser(userId);
  
  return (
    <div>
      <img src={user.avatar} />
      <h2>{user.name}</h2>
      <p>{user.bio}</p>
      <UserStats stats={user.stats} />
    </div>
  );
}

// 子组件接收数据,不再独立获取
function UserStats({ stats }) {
  return (
    <div>
      <span>Posts: {stats.posts}</span>
      <span>Followers: {stats.followers}</span>
    </div>
  );
}

组件粒度的权衡

粒度优点缺点适用场景
粗粒度减少网络往返
减少重复查询
难以独立更新
代码复用性差
数据紧密相关的UI
细粒度易于复用
可独立Suspense
可能重复查询
增加网络开销
数据独立的模块

15.2.5 序列化优化

避免序列化大对象

Server Component通过Flight协议序列化数据,大对象会影响性能:

jsx
// ❌ 不好的做法:传递整个对象
async function ProductPage({ productId }) {
  const product = await db.products.findUnique({
    where: { id: productId },
    include: {
      reviews: true, // 可能有数千条评论
      relatedProducts: true,
      inventory: true,
    },
  });
  
  // 将巨大的对象传递给Client Component
  return <ProductDetails product={product} />;
}
jsx
// ✅ 好的做法:只传递必要的数据
async function ProductPage({ productId }) {
  const product = await db.products.findUnique({
    where: { id: productId },
  });
  
  // 只传递必要的字段
  return (
    <ProductDetails 
      name={product.name}
      price={product.price}
      description={product.description}
    />
  );
}

// 评论独立加载
async function ProductReviews({ productId }) {
  const reviews = await db.reviews.findMany({
    where: { productId },
    take: 10, // 只加载前10条
  });
  
  return <ReviewList reviews={reviews} />;
}

使用流式传输大数据

对于大型数据集,使用流式传输而不是一次性序列化:

jsx
import { Suspense } from 'react';

export default function DataPage() {
  return (
    <div>
      {/* 分批加载数据 */}
      <Suspense fallback={<Loading />}>
        <DataBatch start={0} end={100} />
      </Suspense>
      
      <Suspense fallback={<Loading />}>
        <DataBatch start={100} end={200} />
      </Suspense>
      
      <Suspense fallback={<Loading />}>
        <DataBatch start={200} end={300} />
      </Suspense>
    </div>
  );
}

async function DataBatch({ start, end }) {
  const data = await fetchData(start, end);
  return <DataTable data={data} />;
}

15.3 客户端性能优化

虽然React 19强调服务端优先,但客户端优化依然重要。合理的客户端优化能显著提升用户体验。

15.3.1 React.memo的正确使用

何时使用React.memo

React.memo用于避免不必要的重新渲染,但并非所有组件都需要:

jsx
// ❌ 不必要的memo:组件很简单,重新渲染成本低
const SimpleText = React.memo(({ text }) => {
  return <span>{text}</span>;
});

// ❌ 不必要的memo:props总是变化
const Clock = React.memo(({ time }) => {
  return <div>{time}</div>; // time每秒都变化
});

// ✅ 有价值的memo:组件复杂,props不常变化
const ExpensiveChart = React.memo(({ data, config }) => {
  // 复杂的图表渲染逻辑
  return <ComplexVisualization data={data} config={config} />;
});

// ✅ 有价值的memo:列表项组件
const TodoItem = React.memo(({ todo, onToggle }) => {
  return (
    <li>
      <input 
        type="checkbox" 
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      {todo.text}
    </li>
  );
});

自定义比较函数

默认情况下,React.memo使用浅比较。对于复杂props,可以自定义比较:

jsx
const UserCard = React.memo(
  ({ user, settings }) => {
    return (
      <div>
        <h2>{user.name}</h2>
        <p>{user.email}</p>
        <Settings config={settings} />
      </div>
    );
  },
  (prevProps, nextProps) => {
    // 返回true表示props相等,不需要重新渲染
    return (
      prevProps.user.id === nextProps.user.id &&
      prevProps.user.name === nextProps.user.name &&
      prevProps.user.email === nextProps.user.email &&
      JSON.stringify(prevProps.settings) === JSON.stringify(nextProps.settings)
    );
  }
);

React Compiler的影响

如果使用React Compiler,大多数情况下不需要手动使用React.memo

jsx
// 使用React Compiler时,这个组件会自动被优化
function ProductCard({ product }) {
  // 编译器会自动记忆化这个组件
  return (
    <div>
      <h3>{product.name}</h3>
      <p>{product.price}</p>
    </div>
  );
}

// 无需手动添加React.memo
// const ProductCard = React.memo(function ProductCard({ product }) { ... });

15.3.2 代码分割与懒加载

动态导入

使用React.lazy和动态import()实现代码分割:

jsx
import { lazy, Suspense } from 'react';

// 懒加载组件
const HeavyChart = lazy(() => import('./HeavyChart'));
const AdminPanel = lazy(() => import('./AdminPanel'));

export default function Dashboard({ isAdmin }) {
  return (
    <div>
      <Header />
      
      {/* 图表组件按需加载 */}
      <Suspense fallback={<ChartSkeleton />}>
        <HeavyChart />
      </Suspense>
      
      {/* 管理面板仅管理员可见时才加载 */}
      {isAdmin && (
        <Suspense fallback={<Loading />}>
          <AdminPanel />
        </Suspense>
      )}
    </div>
  );
}

路由级别的代码分割

在Next.js中,页面自动进行代码分割:

jsx
// app/layout.js
export default function RootLayout({ children }) {
  return (
    <html>
      <body>{children}</body>
    </html>
  );
}

// app/page.js - 自动分割为独立chunk
export default function HomePage() {
  return <h1>Home</h1>;
}

// app/dashboard/page.js - 自动分割为独立chunk
export default function DashboardPage() {
  return <Dashboard />;
}

// app/admin/page.js - 自动分割为独立chunk
export default function AdminPage() {
  return <AdminPanel />;
}

组件级别的代码分割

对于大型第三方库,使用动态导入:

jsx
'use client';
import { useState } from 'react';

export function MarkdownEditor() {
  const [Editor, setEditor] = useState(null);
  
  const loadEditor = async () => {
    // 只在用户点击时加载编辑器
    const { default: MDEditor } = await import('react-markdown-editor-lite');
    setEditor(() => MDEditor);
  };
  
  return (
    <div>
      {!Editor ? (
        <button onClick={loadEditor}>打开编辑器</button>
      ) : (
        <Editor />
      )}
    </div>
  );
}

Bundle大小对比

方案初始Bundle按需加载
全部打包500KB0KB
代码分割100KB400KB(按需)

15.3.3 虚拟化长列表

为什么需要虚拟化

渲染大量DOM节点会导致性能问题:

jsx
// ❌ 不好的做法:渲染10000个项目
function ProductList({ products }) {
  return (
    <div>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

// 问题:
// 1. 创建10000个DOM节点
// 2. 占用大量内存
// 3. 滚动性能差

使用react-window

jsx
'use client';
import { FixedSizeList } from 'react-window';

function ProductList({ products }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      <ProductCard product={products[index]} />
    </div>
  );
  
  return (
    <FixedSizeList
      height={600}
      itemCount={products.length}
      itemSize={100}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

// 只渲染可见区域的项目(约6-10个)
// 滚动时动态更新

性能对比

列表大小普通渲染虚拟化渲染
100项流畅流畅
1000项卡顿流畅
10000项严重卡顿流畅

15.3.4 优化事件处理

避免内联函数

内联函数会在每次渲染时创建新的函数引用:

jsx
// ❌ 不好的做法:内联函数
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          // 每次渲染都创建新函数
          onToggle={() => handleToggle(todo.id)}
        />
      ))}
    </ul>
  );
}
jsx
// ✅ 好的做法:使用useCallback
'use client';
import { useCallback } from 'react';

function TodoList({ todos }) {
  const handleToggle = useCallback((id) => {
    // 处理逻辑
  }, []);
  
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={handleToggle}
        />
      ))}
    </ul>
  );
}

const TodoItem = React.memo(({ todo, onToggle }) => {
  return (
    <li onClick={() => onToggle(todo.id)}>
      {todo.text}
    </li>
  );
});

事件委托

对于大量相似元素,使用事件委托:

jsx
'use client';

function TodoList({ todos }) {
  const handleClick = (e) => {
    const todoId = e.target.closest('[data-todo-id]')?.dataset.todoId;
    if (todoId) {
      handleToggle(todoId);
    }
  };
  
  return (
    <ul onClick={handleClick}>
      {todos.map(todo => (
        <li key={todo.id} data-todo-id={todo.id}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

15.3.5 图片优化

使用Next.js Image组件

Next.js的Image组件提供了自动优化:

jsx
import Image from 'next/image';

export function ProductImage({ product }) {
  return (
    <Image
      src={product.imageUrl}
      alt={product.name}
      width={400}
      height={300}
      // 自动优化:
      // 1. 自动选择最佳格式(WebP/AVIF)
      // 2. 响应式图片
      // 3. 懒加载
      // 4. 防止布局偏移
      priority={false} // 非关键图片懒加载
    />
  );
}

响应式图片

jsx
<Image
  src="/hero.jpg"
  alt="Hero"
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  // 根据屏幕大小加载不同尺寸的图片
/>

优先加载关键图片

jsx
export function HeroSection() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero"
      width={1200}
      height={600}
      priority // 首屏图片优先加载
    />
  );
}

15.3.6 状态管理优化

避免不必要的全局状态

jsx
// ❌ 不好的做法:所有状态都放在全局
const GlobalContext = createContext();

function App() {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all');
  
  // 任何状态变化都会导致整个App重新渲染
  return (
    <GlobalContext.Provider value={{ user, theme, todos, filter, ... }}>
      <Layout />
    </GlobalContext.Provider>
  );
}
jsx
// ✅ 好的做法:状态就近管理
function App() {
  return (
    <UserProvider>
      <ThemeProvider>
        <Layout />
      </ThemeProvider>
    </UserProvider>
  );
}

function TodoPage() {
  // todos状态只在TodoPage中管理
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all');
  
  return <TodoList todos={todos} filter={filter} />;
}

使用Context的优化技巧

jsx
// 分离频繁变化的状态
const UserContext = createContext();
const UserDispatchContext = createContext();

function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  
  return (
    <UserContext.Provider value={user}>
      <UserDispatchContext.Provider value={setUser}>
        {children}
      </UserDispatchContext.Provider>
    </UserContext.Provider>
  );
}

// 只读取user的组件不会因为setUser变化而重新渲染
function UserProfile() {
  const user = useContext(UserContext);
  return <div>{user.name}</div>;
}

// 只需要setUser的组件不会因为user变化而重新渲染
function LoginButton() {
  const setUser = useContext(UserDispatchContext);
  return <button onClick={() => setUser(...)}>Login</button>;
}

15.3.7 Transition优化

使用useTransition标记非紧急更新

jsx
'use client';
import { useState, useTransition } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();
  
  const handleSearch = (e) => {
    const value = e.target.value;
    setQuery(value); // 紧急更新:立即更新输入框
    
    // 非紧急更新:搜索结果可以延迟
    startTransition(() => {
      const filtered = performExpensiveSearch(value);
      setResults(filtered);
    });
  };
  
  return (
    <div>
      <input 
        value={query} 
        onChange={handleSearch}
        // 输入框始终响应流畅
      />
      
      {isPending && <Spinner />}
      
      <SearchResults results={results} />
    </div>
  );
}

useDeferredValue延迟更新

jsx
'use client';
import { useState, useDeferredValue } from 'react';

function ProductFilter({ products }) {
  const [filter, setFilter] = useState('');
  const deferredFilter = useDeferredValue(filter);
  
  // 使用延迟的filter值进行过滤
  const filteredProducts = products.filter(p => 
    p.name.includes(deferredFilter)
  );
  
  return (
    <div>
      <input 
        value={filter}
        onChange={e => setFilter(e.target.value)}
        // 输入框立即响应
      />
      
      {/* 列表更新可以延迟 */}
      <ProductList products={filteredProducts} />
    </div>
  );
}

15.4 性能分析工具

工欲善其事,必先利其器。掌握性能分析工具是优化的前提。

15.4.1 React DevTools Profiler

启用Profiler

React DevTools提供了强大的性能分析功能:

  1. 安装React DevTools浏览器扩展
  2. 打开DevTools,切换到"Profiler"标签
  3. 点击"Record"开始录制
  4. 执行需要分析的操作
  5. 点击"Stop"停止录制

分析渲染性能

Profiler会显示每个组件的渲染时间:

Flamegraph视图:
┌─ App (2.3ms)
│  ├─ Header (0.5ms)
│  ├─ Sidebar (0.8ms)
│  └─ Content (1.0ms)
│     ├─ ProductList (0.7ms)
│     │  └─ ProductCard (0.1ms × 5)
│     └─ Pagination (0.3ms)

识别性能问题

  1. 频繁重新渲染

    • 组件在Profiler中多次出现
    • 可能需要使用React.memouseMemo
  2. 渲染时间过长

    • 单个组件耗时超过16ms(60fps)
    • 可能需要代码分割或虚拟化
  3. 不必要的渲染

    • 组件渲染但没有视觉变化
    • 检查props是否真的变化了

Profiler API

在代码中使用Profiler组件:

jsx
import { Profiler } from 'react';

function onRenderCallback(
  id, // 组件的"id"
  phase, // "mount"或"update"
  actualDuration, // 本次更新花费的时间
  baseDuration, // 不使用memoization的情况下渲染整棵子树需要的时间
  startTime, // 本次更新开始的时间
  commitTime, // 本次更新提交的时间
  interactions // 本次更新的交互集合
) {
  console.log(`${id} (${phase}) took ${actualDuration}ms`);
}

export default function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <Dashboard />
    </Profiler>
  );
}

15.4.2 Chrome DevTools Performance

录制性能

  1. 打开Chrome DevTools
  2. 切换到"Performance"标签
  3. 点击"Record"
  4. 执行操作
  5. 点击"Stop"

分析指标

关键指标解读

指标含义目标值
FCP首次内容绘制< 1.8s
LCP最大内容绘制< 2.5s
FID首次输入延迟< 100ms
CLS累积布局偏移< 0.1
TTI可交互时间< 3.8s

识别长任务

Performance面板会标记超过50ms的长任务:

Main Thread:
├─ Task (120ms) ⚠️ Long Task
│  ├─ Parse HTML (20ms)
│  ├─ Evaluate Script (80ms) ← 瓶颈
│  └─ Layout (20ms)
├─ Task (30ms) ✓
└─ Task (45ms) ✓

15.4.3 Lighthouse

运行Lighthouse

  1. 打开Chrome DevTools
  2. 切换到"Lighthouse"标签
  3. 选择分析类型(Performance、Accessibility等)
  4. 点击"Analyze page load"

性能评分

Lighthouse会给出0-100的性能评分:

Performance: 85/100

Metrics:
├─ First Contentful Paint: 1.2s ✓
├─ Largest Contentful Paint: 2.1s ✓
├─ Total Blocking Time: 150ms ⚠️
├─ Cumulative Layout Shift: 0.05 ✓
└─ Speed Index: 2.5s ✓

Opportunities:
├─ Reduce unused JavaScript (Save 120KB)
├─ Properly size images (Save 80KB)
└─ Enable text compression (Save 45KB)

优化建议

Lighthouse会提供具体的优化建议:

  1. 减少未使用的JavaScript

    • 使用代码分割
    • 移除未使用的依赖
  2. 优化图片

    • 使用现代格式(WebP、AVIF)
    • 压缩图片
    • 使用响应式图片
  3. 启用文本压缩

    • 配置gzip或Brotli压缩
  4. 减少主线程工作

    • 优化JavaScript执行
    • 使用Web Workers

15.4.4 Next.js Analytics

启用Next.js Analytics

javascript
// next.config.js
module.exports = {
  experimental: {
    instrumentationHook: true,
  },
};
javascript
// instrumentation.js
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    // 服务端监控
    await import('./instrumentation-node');
  }
  
  if (process.env.NEXT_RUNTIME === 'edge') {
    // Edge运行时监控
    await import('./instrumentation-edge');
  }
}

Web Vitals监控

jsx
// app/layout.js
import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/next';

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

自定义性能监控

jsx
// app/layout.js
'use client';
import { useReportWebVitals } from 'next/web-vitals';

export function WebVitals() {
  useReportWebVitals((metric) => {
    console.log(metric);
    
    // 发送到分析服务
    if (metric.label === 'web-vital') {
      fetch('/api/analytics', {
        method: 'POST',
        body: JSON.stringify({
          name: metric.name,
          value: metric.value,
          id: metric.id,
        }),
      });
    }
  });
  
  return null;
}

15.4.5 服务端性能监控

Node.js性能监控

javascript
// instrumentation-node.js
import { performance } from 'perf_hooks';

export async function register() {
  // 监控数据库查询
  const originalQuery = db.query;
  db.query = async function(...args) {
    const start = performance.now();
    const result = await originalQuery.apply(this, args);
    const duration = performance.now() - start;
    
    if (duration > 100) {
      console.warn(`Slow query (${duration}ms):`, args[0]);
    }
    
    return result;
  };
  
  // 监控API响应时间
  app.use((req, res, next) => {
    const start = performance.now();
    
    res.on('finish', () => {
      const duration = performance.now() - start;
      console.log(`${req.method} ${req.url}: ${duration}ms`);
    });
    
    next();
  });
}

APM工具集成

使用专业的APM(Application Performance Monitoring)工具:

javascript
// 使用New Relic
import newrelic from 'newrelic';

export async function GET(request) {
  return newrelic.startWebTransaction('/api/users', async () => {
    const users = await db.users.findMany();
    return Response.json(users);
  });
}

// 使用Datadog
import tracer from 'dd-trace';
tracer.init();

export async function GET(request) {
  const span = tracer.startSpan('api.users');
  try {
    const users = await db.users.findMany();
    return Response.json(users);
  } finally {
    span.finish();
  }
}

15.4.6 实时用户监控(RUM)

收集真实用户数据

jsx
'use client';
import { useEffect } from 'react';

export function RUMCollector() {
  useEffect(() => {
    // 监控页面加载性能
    if (typeof window !== 'undefined' && window.performance) {
      const perfData = window.performance.getEntriesByType('navigation')[0];
      
      const metrics = {
        dns: perfData.domainLookupEnd - perfData.domainLookupStart,
        tcp: perfData.connectEnd - perfData.connectStart,
        ttfb: perfData.responseStart - perfData.requestStart,
        download: perfData.responseEnd - perfData.responseStart,
        domParse: perfData.domContentLoadedEventEnd - perfData.responseEnd,
        total: perfData.loadEventEnd - perfData.fetchStart,
      };
      
      // 发送到分析服务
      sendToAnalytics('page-load', metrics);
    }
    
    // 监控资源加载
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.duration > 1000) {
          sendToAnalytics('slow-resource', {
            name: entry.name,
            duration: entry.duration,
          });
        }
      }
    });
    
    observer.observe({ entryTypes: ['resource'] });
    
    return () => observer.disconnect();
  }, []);
  
  return null;
}

性能预算

设置性能预算,自动检测性能退化:

javascript
// performance-budget.js
export const performanceBudget = {
  // Bundle大小预算
  bundles: {
    main: 200 * 1024, // 200KB
    vendor: 300 * 1024, // 300KB
  },
  
  // 性能指标预算
  metrics: {
    fcp: 1800, // 1.8s
    lcp: 2500, // 2.5s
    fid: 100, // 100ms
    cls: 0.1,
  },
  
  // 资源预算
  resources: {
    images: 500 * 1024, // 500KB
    fonts: 100 * 1024, // 100KB
    scripts: 400 * 1024, // 400KB
  },
};

// 在CI中检查
if (actualSize > performanceBudget.bundles.main) {
  throw new Error(`Main bundle exceeds budget: ${actualSize} > ${performanceBudget.bundles.main}`);
}

15.5 示例:大型应用性能优化

通过一个实际案例,展示如何系统地优化大型React应用的性能。

15.5.1 案例背景

应用概况

一个电商平台的商品列表页面,存在以下性能问题:

  • 首屏加载时间:5.2秒
  • LCP(最大内容绘制):4.8秒
  • TTI(可交互时间):6.1秒
  • JavaScript Bundle:1.2MB
  • 用户反馈:页面加载慢,滚动卡顿

页面结构

jsx
// 优化前的代码结构
'use client';
import { useState, useEffect } from 'react';
import { ProductCard } from './ProductCard';
import { FilterPanel } from './FilterPanel';
import { SortDropdown } from './SortDropdown';

export default function ProductListPage() {
  const [products, setProducts] = useState([]);
  const [filters, setFilters] = useState({});
  const [sort, setSort] = useState('popular');
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetchProducts(filters, sort).then(data => {
      setProducts(data);
      setLoading(false);
    });
  }, [filters, sort]);
  
  if (loading) return <Loading />;
  
  return (
    <div>
      <FilterPanel filters={filters} onChange={setFilters} />
      <SortDropdown value={sort} onChange={setSort} />
      <div className="product-grid">
        {products.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </div>
  );
}

15.5.2 性能分析

使用Lighthouse分析

Performance Score: 42/100

Metrics:
├─ FCP: 2.8s ⚠️
├─ LCP: 4.8s ❌
├─ TBT: 890ms ❌
├─ CLS: 0.15 ⚠️
└─ Speed Index: 4.2s ❌

Opportunities:
├─ Reduce unused JavaScript: 450KB
├─ Serve images in next-gen formats: 320KB
├─ Eliminate render-blocking resources: 1.2s
└─ Reduce server response time: 800ms

使用React DevTools Profiler分析

发现的问题:

  1. 整个页面都是Client Component

    • 所有代码都发送到客户端
    • 数据在客户端获取,增加网络往返
  2. ProductCard频繁重新渲染

    • filters或sort变化时,所有ProductCard都重新渲染
    • 没有使用React.memo
  3. 大型依赖库

    • 使用了完整的lodash(70KB)
    • 使用了完整的moment.js(230KB)
  4. 图片未优化

    • 使用原始大小的图片
    • 未使用现代格式(WebP)

15.5.3 优化方案

第一步:服务端渲染 + RSC

将数据获取移到服务端:

jsx
// app/products/page.js
import { db } from '@/lib/database';
import { ProductGrid } from './ProductGrid';
import { FilterPanel } from './FilterPanel';

export default async function ProductListPage({ searchParams }) {
  // 在服务端获取数据
  const products = await db.products.findMany({
    where: buildFilters(searchParams),
    orderBy: buildSort(searchParams.sort),
    include: {
      images: true,
      reviews: {
        select: {
          rating: true,
        },
      },
    },
  });
  
  // 计算平均评分
  const productsWithRating = products.map(p => ({
    ...p,
    avgRating: p.reviews.reduce((sum, r) => sum + r.rating, 0) / p.reviews.length,
  }));
  
  return (
    <div>
      {/* Server Component */}
      <FilterPanel />
      
      {/* Client Component - 只负责交互 */}
      <ProductGrid products={productsWithRating} />
    </div>
  );
}

function buildFilters(params) {
  const filters = {};
  
  if (params.category) {
    filters.categoryId = params.category;
  }
  
  if (params.minPrice) {
    filters.price = { gte: parseFloat(params.minPrice) };
  }
  
  if (params.maxPrice) {
    filters.price = { ...filters.price, lte: parseFloat(params.maxPrice) };
  }
  
  return filters;
}

function buildSort(sort) {
  switch (sort) {
    case 'price-asc':
      return { price: 'asc' };
    case 'price-desc':
      return { price: 'desc' };
    case 'rating':
      return { avgRating: 'desc' };
    default:
      return { popularity: 'desc' };
  }
}

第二步:优化Client Component

jsx
// app/products/ProductGrid.js
'use client';
import { memo } from 'react';
import { FixedSizeGrid } from 'react-window';
import { ProductCard } from './ProductCard';

export function ProductGrid({ products }) {
  const columnCount = 4;
  const rowCount = Math.ceil(products.length / columnCount);
  
  const Cell = ({ columnIndex, rowIndex, style }) => {
    const index = rowIndex * columnCount + columnIndex;
    const product = products[index];
    
    if (!product) return null;
    
    return (
      <div style={style}>
        <ProductCard product={product} />
      </div>
    );
  };
  
  return (
    <FixedSizeGrid
      columnCount={columnCount}
      columnWidth={300}
      height={800}
      rowCount={rowCount}
      rowHeight={400}
      width={1200}
    >
      {Cell}
    </FixedSizeGrid>
  );
}

// 使用React.memo避免不必要的重新渲染
export const ProductCard = memo(function ProductCard({ product }) {
  return (
    <div className="product-card">
      <ProductImage src={product.images[0].url} alt={product.name} />
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <Rating value={product.avgRating} />
    </div>
  );
});

第三步:图片优化

jsx
// app/products/ProductImage.js
import Image from 'next/image';

export function ProductImage({ src, alt }) {
  return (
    <Image
      src={src}
      alt={alt}
      width={300}
      height={300}
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 300px"
      loading="lazy"
      // Next.js自动优化为WebP/AVIF
    />
  );
}

第四步:代码分割

jsx
// app/products/FilterPanel.js
'use client';
import { lazy, Suspense } from 'react';

// 懒加载高级筛选面板
const AdvancedFilters = lazy(() => import('./AdvancedFilters'));

export function FilterPanel() {
  const [showAdvanced, setShowAdvanced] = useState(false);
  
  return (
    <div>
      <BasicFilters />
      
      <button onClick={() => setShowAdvanced(true)}>
        高级筛选
      </button>
      
      {showAdvanced && (
        <Suspense fallback={<Loading />}>
          <AdvancedFilters />
        </Suspense>
      )}
    </div>
  );
}

第五步:优化依赖

javascript
// 替换lodash
// ❌ import _ from 'lodash';
// ✅ import debounce from 'lodash/debounce';

// 替换moment.js
// ❌ import moment from 'moment';
// ✅ import { formatDistance } from 'date-fns';

第六步:添加Suspense边界

jsx
// app/products/page.js
import { Suspense } from 'react';

export default function ProductListPage({ searchParams }) {
  return (
    <div>
      <FilterPanel />
      
      {/* 产品列表独立加载 */}
      <Suspense fallback={<ProductGridSkeleton />}>
        <ProductList searchParams={searchParams} />
      </Suspense>
      
      {/* 推荐商品独立加载 */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations />
      </Suspense>
    </div>
  );
}

async function ProductList({ searchParams }) {
  const products = await fetchProducts(searchParams);
  return <ProductGrid products={products} />;
}

async function Recommendations() {
  const recommendations = await fetchRecommendations();
  return <RecommendationList products={recommendations} />;
}

15.5.4 优化效果

性能指标对比

指标优化前优化后改善
Lighthouse Score4294+124%
FCP2.8s0.9s-68%
LCP4.8s1.6s-67%
TTI6.1s2.1s-66%
TBT890ms120ms-87%
CLS0.150.02-87%

Bundle大小对比

资源优化前优化后减少
JavaScript1.2MB280KB-77%
Images850KB180KB-79%
Total2.05MB460KB-78%

用户体验改善

15.5.5 优化清单

服务端优化

  • [x] 使用Server Components减少客户端JavaScript
  • [x] 在服务端获取数据,避免客户端网络请求
  • [x] 使用数据库JOIN避免N+1查询
  • [x] 实现数据缓存策略
  • [x] 使用Suspense实现流式渲染

客户端优化

  • [x] 使用React.memo避免不必要的重新渲染
  • [x] 使用虚拟化处理长列表
  • [x] 实现代码分割和懒加载
  • [x] 优化事件处理函数
  • [x] 使用useTransition处理非紧急更新

资源优化

  • [x] 使用Next.js Image组件优化图片
  • [x] 启用图片懒加载
  • [x] 使用现代图片格式(WebP/AVIF)
  • [x] 压缩和优化JavaScript
  • [x] 移除未使用的依赖

监控与持续优化

  • [x] 集成性能监控工具
  • [x] 设置性能预算
  • [x] 定期进行性能审计
  • [x] 收集真实用户数据(RUM)
  • [x] 建立性能优化文化

本章小结

本章系统讲解了React 19全栈应用的性能优化策略:

  1. 全栈优化原则

    • 服务端优先渲染,充分利用服务器性能
    • 减少客户端JavaScript,降低Bundle大小
    • 合理划分组件边界,最小化Client Component范围
    • 优化数据获取,避免瀑布式请求
    • 使用流式渲染,实现渐进式加载
  2. RSC性能优化

    • 避免N+1查询,使用JOIN或DataLoader
    • 合理设置Suspense边界,实现细粒度流式渲染
    • 使用React Cache API和fetch缓存
    • 控制组件粒度,避免过度细分
    • 优化序列化,避免传递大对象
  3. 客户端性能优化

    • 正确使用React.memo,避免不必要的重新渲染
    • 实现代码分割和懒加载,减少初始Bundle
    • 使用虚拟化处理长列表
    • 优化事件处理和状态管理
    • 使用Transition API处理非紧急更新
  4. 性能分析工具

    • React DevTools Profiler分析组件渲染
    • Chrome DevTools Performance分析整体性能
    • Lighthouse进行综合性能评估
    • Next.js Analytics监控真实用户数据
    • 服务端性能监控和APM工具
  5. 实战案例

    • 通过系统的性能优化,将Lighthouse评分从42提升到94
    • 首屏加载时间从5.2秒降低到1.2秒
    • JavaScript Bundle从1.2MB减少到280KB
    • 用户体验显著改善

性能优化是一个持续的过程,需要在开发的各个阶段都保持关注。React 19的全栈架构为性能优化提供了强大的工具,但关键在于理解原理并正确使用。通过服务端优先、合理的组件边界划分、智能的缓存策略,我们可以构建出既功能强大又性能卓越的React应用。


思考题

  1. 在什么情况下应该使用Server Component,什么情况下应该使用Client Component?如何在两者之间找到平衡?

  2. React Cache API和Next.js的fetch缓存有什么区别?它们分别适用于什么场景?

  3. 虚拟化列表能够显著提升长列表的性能,但它也有一些限制。请思考虚拟化列表在哪些场景下不适用?

  4. useTransition和useDeferredValue都可以用于优化性能,它们有什么区别?应该如何选择?

  5. 在实际项目中,如何建立性能优化的流程和文化?如何确保性能不会随着项目发展而退化?


下一章预告

至此,我们已经完成了React 19全栈源码的系统学习。从基础架构到客户端实现,从服务端渲染到性能优化,我们深入探讨了React作为全栈框架的方方面面。

附录部分将提供实用的参考资料,包括源码调试技巧、核心API速查表和术语表,帮助你在实际开发中快速查阅和应用所学知识。