React性能优化实战指南 - 让你的组件飞起来

118 阅读4分钟

前言

相信很多小伙伴都遇到过这样的情况:明明只是改了一个小小的状态,整个页面却像中了病毒一样疯狂重渲染,卡得要死要活的。别慌,今天我就带大家深入剖析React的渲染机制,手把手教你写出丝滑流畅的高性能组件!

先来聊聊React的渲染顺序

很多小伙伴可能不知道,React组件的渲染是有严格顺序的:

  • 执行阶段:从外到内,像剥洋葱一样一层层往里执行
  • 完成阶段:从内到外,像搭积木一样从底层开始完成挂载

这就像盖房子,先搭框架(父组件),再装修细节(子组件),但是验收的时候要从房间开始,最后才是整栋楼。

真正的痛点:无意义的重渲染

来看这个经典的场景:

function App() {
  const [count, setCount] = useState(0)
  const [num, setNum] = useState(0)
  console.log('App render')

  return (
    <>
      <div>{count}</div>
      <button onClick={() => setCount(count + 1)}>+</button>
      <Button num={num}>Click Me</Button>
    </>
  )
}

当我们点击按钮改变count的时候,按理说Button组件应该淡定如山,毕竟它只关心num啊!但现实很骨感,Button还是会无辜地重新渲染。

这就像你在家里换个灯泡,结果邻居家的狗都跟着汪汪叫,完全没必要啊!

救星登场:React.memo

这时候我们的救星React.memo闪亮登场!它就像给组件加了一层保护罩,只有props真正变化时才会重渲染:

import { useEffect, memo } from 'react'

const Button = ({ num, onClick, children }) => {
    useEffect(() => {
      console.log('Button UseEffect')
    }, [])
    console.log('Button render')
    return <button onClick={onClick}>{num} {children}</button>
}

// 高阶组件,给Button穿上防护服
export default memo(Button)

有了这层保护,Button组件就能安心做自己的事情,不会被父组件的其他状态变化所打扰。

useMemo:拯救昂贵计算

有时候我们需要做一些复杂的计算,比如:

const expensiveComputation = (n) => {
  // 这里模拟一个超级耗时的计算
  console.log('expensiveComputation')
  for(let i = 0; i < 1000000000; i++) {
    i++
  }
  return n * 2
}

每次组件重渲染都要跑这么重的计算?那还不如回家种田!

这时候useMemo就派上用场了:

const result = useMemo(() => expensiveComputation(num), [num])

这样只有当num真正变化时,才会重新计算。其他时候就像个优雅的贵妇,端着茶杯淡定地说:"不关我事,我不算。"

useCallback:函数也要缓存

还有一个常被忽视的性能杀手:函数的重新创建。

// 错误示范:每次渲染都创建新函数
const handleClick = () => {
  console.log('handleClick')
}

// 正确姿势:用useCallback缓存函数
const handleClick = useCallback(() => {
  console.log('handleClick')
}, [num])

配合React.memo使用,效果更佳!因为如果每次都传入新的函数引用,memo的浅比较就失效了,又回到了原点。

完整的性能优化实战代码

来看看我们的完整优化方案:

import {
   useState,
   useEffect,
   useCallback,
   useMemo
} from 'react'
import './App.css'
import Button from './components/Button'

function App() {
  const [count, setCount] = useState(0)
  const [num, setNum] = useState(0)
  console.log('App render')

  const expensiveComputation = (n) => {
    // 复杂计算 开销高
    console.log('expensiveComputation')
    for(let i = 0; i < 1000000000; i++) {
      i++
    }
    return n*2
  }
  
  // 缓存计算结果
  const result = useMemo(() => expensiveComputation(num), [num])
  
  useEffect(() => {
    console.log('count', count)
  }, [count])
  
  useEffect(() => {
    console.log('num', num)
  }, [num])
  
  // 缓存事件处理函数
  const handleClick = useCallback(() => {
    console.log('handleClick')
  }, [num])
  
  return (
    <>
      <div>{result}</div>
      <div>{count}</div>
      <button onClick={() => setCount(count + 1)}>+</button>
      <br />
      <button onClick={() => setNum(num + 1)}>+</button>
      <br />
      <Button num={num} onClick={handleClick}>Click Me Me</Button>
    </>
  )
}

export default App

组件拆分的艺术

说到性能优化,不得不提组件拆分的粒度问题。

好的拆分策略:

  • 单一职责:每个组件只负责一件事
  • 状态隔离:相关的状态放在一起,无关的分开
  • 复用性考虑:可复用的逻辑抽成独立组件

避免的陷阱:

  • 不要把所有状态都塞进一个Context
  • 不要过度拆分导致props传递地狱
  • 不要为了拆分而拆分

Context使用的坑

很多小伙伴喜欢把所有状态都扔进一个大Context里,觉得这样很方便。但这样做的后果就是:任何一个状态变化,所有使用这个Context的组件都会重渲染!

// 错误示范:把所有鸡蛋放在一个篮子里
const AppContext = createContext({
  user: null,
  theme: 'light',
  todos: [],
  cart: [],
  // ... 更多状态
})

// 正确做法:按功能拆分Context
const UserContext = createContext()
const ThemeContext = createContext()
const TodoContext = createContext()

这样做的好处是显而易见的:用户信息变化时,只有关心用户信息的组件会重渲染,主题切换不会影响到购物车组件。

总结 🎉

React的性能优化说白了就是减少不必要的重渲染和重复计算。通过合理使用React.memouseMemouseCallback这些工具,配合良好的组件设计,我们就能写出既优雅又高性能的React应用。 性能优化是一门艺术,需要在用户体验、开发效率和代码可维护性之间找到最佳平衡点。不要为了那0.1毫秒的提升而让代码变得晦涩难懂。