2024年01月02日9 分钟

React 性能优化全链路:从组件到架构的系统性方案

覆盖 React 应用性能优化的完整链路,从组件级优化到架构级方案,附带实战代码和性能对比

React 性能优化全景

组件级 → 状态管理级 → 渲染级 → 数据级 → 架构级
  │         │          │        │        │
  memo     状态下沉     虚拟列表  请求优化  SSR/SSG
  useMemo  状态拆分     并发渲染  缓存     代码分割
  useCallback 不可变数据 Suspense 预加载   懒加载

一、组件级优化

1. React.memo — 阻止无效重渲染

// 场景:父组件频繁更新,子组件 props 没变
function Parent() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <ExpensiveList items={staticItems} />  {/* count 变了,items 没变 */}
    </div>
  )
}

// ❌ 每次 Parent 渲染,ExpensiveList 都会重渲染
function ExpensiveList({ items }) {
  return items.map(item => <Item key={item.id} {...item} />)
}

// ✅ memo 包裹:props 不变就不重渲染
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
  return items.map(item => <Item key={item.id} {...item} />)
})

2. useMemo + useCallback — 稳定引用

function SearchPage({ data }) {
  const [query, setQuery] = useState('')
  const [sort, setSort] = useState('date')

  // ✅ 只在 data/query/sort 变化时重新计算
  const filtered = useMemo(() => {
    return data
      .filter(item => item.title.includes(query))
      .sort((a, b) => sort === 'date' ? b.date - a.date : b.score - a.score)
  }, [data, query, sort])

  // ✅ 稳定的函数引用,传给 memo 子组件时不会导致重渲染
  const handleItemClick = useCallback((id: string) => {
    navigate(`/item/${id}`)
  }, [])

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <MemoizedList items={filtered} onItemClick={handleItemClick} />
    </div>
  )
}

3. 避免在渲染中创建新对象/数组

// ❌ 每次渲染都创建新对象 → memo 失效
function Parent() {
  return <Child style={{ color: 'red' }} data={[1, 2, 3]} />
  //                   ↑ 新对象             ↑ 新数组
}

// ✅ 提到组件外面或用 useMemo
const style = { color: 'red' }
const data = [1, 2, 3]

function Parent() {
  return <Child style={style} data={data} />
}

二、状态管理优化

1. 状态下沉(State Colocation)

// ❌ 状态放在顶层 → 整棵树重渲染
function App() {
  const [inputValue, setInputValue] = useState('')  // 每次输入全 App 重渲染

  return (
    <div>
      <Header />  {/* 不需要 inputValue,但被迫重渲染 */}
      <Sidebar /> {/* 不需要 inputValue,但被迫重渲染 */}
      <input value={inputValue} onChange={e => setInputValue(e.target.value)} />
      <SearchResults query={inputValue} />
    </div>
  )
}

// ✅ 状态下沉到需要的地方
function App() {
  return (
    <div>
      <Header />
      <Sidebar />
      <SearchSection />  {/* 状态封装在内部 */}
    </div>
  )
}

function SearchSection() {
  const [inputValue, setInputValue] = useState('')  // 只影响这个组件
  return (
    <>
      <input value={inputValue} onChange={e => setInputValue(e.target.value)} />
      <SearchResults query={inputValue} />
    </>
  )
}

2. Context 拆分

// ❌ 大 Context → 任何值变化都导致所有消费者重渲染
const AppContext = createContext({ user: null, theme: 'dark', locale: 'zh' })
// theme 变了 → 只用 user 的组件也重渲染

// ✅ 拆分为独立 Context
const UserContext = createContext(null)
const ThemeContext = createContext('dark')
const LocaleContext = createContext('zh')
// theme 变了 → 只有用 ThemeContext 的组件重渲染

3. 使用 useReducer 管理复杂状态

// 多个相关 useState → 多次渲染
const [name, setName] = useState('')
const [age, setAge] = useState(0)
const [email, setEmail] = useState('')
// 同时更新三个 → 可能触发多次渲染

// useReducer → 一次 dispatch 更新所有状态
const [state, dispatch] = useReducer(reducer, { name: '', age: 0, email: '' })
dispatch({ type: 'UPDATE_ALL', payload: { name: 'Alice', age: 25, email: 'a@b.com' } })
// 只触发一次渲染

三、渲染优化

1. 虚拟列表

import { FixedSizeList } from 'react-window'

// 10000 条数据 → 只渲染可见的 ~20 条
function VirtualList({ items }) {
  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={60}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style} className="flex items-center px-4">
          <span>{items[index].name}</span>
        </div>
      )}
    </FixedSizeList>
  )
}

// DOM 节点数: 10000 → ~25(减少 99.75%)

2. React 18 并发特性

// useTransition:标记低优先级更新
function SearchWithTransition() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  const [isPending, startTransition] = useTransition()

  const handleChange = (e) => {
    setQuery(e.target.value)  // 高优先级:输入框立即响应

    startTransition(() => {
      setResults(filterData(e.target.value))  // 低优先级:可以延迟
    })
  }

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending && <Spinner />}
      <ResultList data={results} />
    </div>
  )
}

// useDeferredValue:延迟更新某个值
function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query)
  // query 立即变化(输入框响应快)
  // deferredQuery 延迟变化(列表渲染不阻塞输入)

  const filtered = useMemo(
    () => heavyFilter(data, deferredQuery),
    [deferredQuery]
  )

  return <List items={filtered} />
}

3. Suspense 流式加载

function ProfilePage() {
  return (
    <div>
      {/* 立即显示 */}
      <h1>用户主页</h1>

      {/* 数据加载中显示骨架屏,加载完替换 */}
      <Suspense fallback={<ProfileSkeleton />}>
        <ProfileDetails />
      </Suspense>

      <Suspense fallback={<PostsSkeleton />}>
        <UserPosts />
      </Suspense>

      {/* 两个 Suspense 独立加载,互不阻塞 */}
    </div>
  )
}

四、数据层优化

1. 请求去重和缓存

// React Query / SWR 自动缓存和去重
import { useQuery } from '@tanstack/react-query'

function UserProfile({ id }) {
  const { data, isLoading } = useQuery({
    queryKey: ['user', id],
    queryFn: () => fetchUser(id),
    staleTime: 5 * 60 * 1000,  // 5 分钟内不重新请求
    cacheTime: 30 * 60 * 1000, // 缓存保留 30 分钟
  })

  // 多个组件用相同的 queryKey → 只发一次请求
  // 后续访问 → 直接从缓存读取
}

2. 预加载

// 路由预加载:鼠标悬停时提前加载
function NavLink({ to, children }) {
  const prefetchPage = () => {
    const Component = lazy(() => import(`./pages/${to}`))
    // 触发代码和数据的预加载
  }

  return (
    <Link to={to} onMouseEnter={prefetchPage}>
      {children}
    </Link>
  )
}

// Next.js 自动预加载
import Link from 'next/link'
<Link href="/about">About</Link>
// Next.js 会在链接出现在视口时自动预加载 /about 的代码

五、架构级优化

1. 代码分割策略

// 路由级分割
const Home = lazy(() => import('./pages/Home'))
const Blog = lazy(() => import('./pages/Blog'))
const Chat = lazy(() => import('./pages/Chat'))

// 组件级分割
const HeavyChart = lazy(() => import('./components/Chart'))
const MarkdownEditor = lazy(() => import('./components/Editor'))

// 库级分割
const pdf = () => import('pdfjs-dist').then(m => m.default)

2. 选择合适的渲染模式

页面类型        →  渲染模式
─────────────────────────────
博客文章        →  SSG(构建时生成)
商品列表        →  ISR(定时更新)
用户 Dashboard  →  CSR(客户端渲染)
搜索结果        →  SSR(实时渲染)

性能分析工具

React DevTools Profiler:
  1. 打开 React DevTools → Profiler 面板
  2. 点击录制 → 操作页面 → 停止录制
  3. 查看每个组件的渲染次数和耗时
  4. 识别不必要的重渲染(灰色 = 跳过,彩色 = 渲染)

Chrome DevTools Performance:
  1. 打开 Performance 面板 → 录制
  2. 查看火焰图:哪些函数耗时最长
  3. 查看 Long Tasks(红色标记)
  4. 查看布局偏移时刻

why-did-you-render:
  import whyDidYouRender from '@welldone-software/why-did-you-render'
  whyDidYouRender(React, { trackAllPureComponents: true })
  // 在控制台打印不必要重渲染的原因

优化决策流程

页面/组件卡顿?
  │
  ├─ Profiler 分析:是否有不必要的重渲染?
  │   ├─ 是 → React.memo + useMemo + useCallback
  │   └─ 否 → 继续
  │
  ├─ Performance 分析:是否有长任务?
  │   ├─ 是 → 代码分割 / Web Worker / useTransition
  │   └─ 否 → 继续
  │
  ├─ DOM 节点太多?
  │   ├─ 是 → 虚拟列表 (react-window)
  │   └─ 否 → 继续
  │
  ├─ 网络请求慢?
  │   ├─ 是 → 缓存 / 预加载 / SSG
  │   └─ 否 → 继续
  │
  └─ Bundle 太大?
      └─ 是 → 代码分割 / Tree Shaking / 依赖替换