React 的声明式 API 让 UI 开发变得高效,但如果不注意性能,组件的频繁重渲染会导致页面卡顿。本文从 React 的渲染机制出发,介绍几种最常见、最有效的优化手段。

1. 理解 React 的渲染流程

React 组件在以下情况会触发重新渲染:

  1. 组件自身的 state 发生变化(useStateuseReducer)。
  2. 父组件重新渲染,导致子组件默认也会被渲染。
  3. Context 的值发生变化,消费该 Context 的组件重新渲染。

这意味着,即使子组件的 props 没有变化,它也可能因为父组件更新而白白渲染一次。

2. 使用 React.memo 避免无效渲染

React.memo 是一个高阶组件,用于对函数组件进行浅比较。如果 props 没有变化,就跳过本次渲染。

1
2
3
4
5
6
7
8
const UserCard = React.memo(({ name, avatar }) => {
return (
<div className="user-card">
<img src={avatar} alt={name} />
<span>{name}</span>
</div>
)
})

注意:如果 props 包含函数或对象,React.memo 的浅比较会失效。此时可以配合 useCallbackuseMemo 使用。

3. useCallback 缓存函数引用

父组件传递给子组件的回调函数,每次渲染都是新的引用。用 useCallback 可以缓存它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Parent() {
const [count, setCount] = useState(0)

const handleClick = useCallback(() => {
console.log('clicked')
}, [])

return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<Child onClick={handleClick} />
</div>
)
}

4. useMemo 缓存计算结果

对于复杂的计算逻辑,可以用 useMemo 避免每次渲染都重新执行:

1
2
3
const sortedList = useMemo(() => {
return list.sort((a, b) => b.score - a.score)
}, [list])

同样,useMemo 也可以用来缓存对象,配合 React.memo 稳定子组件的 props 引用。

5. 状态下沉与组件拆分

一个常见的反模式是把所有状态放在最顶层组件。状态变化时,整个页面都会重渲染。更好的做法是:

  • 状态下沉:让状态尽量靠近使用它的组件。
  • 合理拆分:将不依赖该状态的 UI 拆成独立组件,避免被波及。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 不推荐:表单状态放在 Page 级别
function Page() {
const [form, setForm] = useState({})
return (
<div>
<Header />
<Form form={form} setForm={setForm} />
<Footer />
</div>
)
}

// 推荐:Form 内部自己管理状态
function Page() {
return (
<div>
<Header />
<Form />
<Footer />
</div>
)
}

6. 延迟加载与代码分割

对于大型应用,可以使用 React.lazySuspense 进行路由级别的代码分割,减少首屏加载时间:

1
2
3
4
5
6
7
8
9
const Dashboard = React.lazy(() => import('./Dashboard'))

function App() {
return (
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
)
}

总结

React 性能优化从来不是盲目使用 useMemoReact.memo,而是建立在对渲染机制的理解之上。核心原则:

  1. 先定位性能瓶颈(React DevTools Profiler)。
  2. 通过拆分组件、下沉状态减少渲染范围。
  3. 在必要时用 memouseCallbackuseMemo 稳定引用。
  4. 对大页面做代码分割,优化首屏体验。