React 性能优化实践 - 精细化渲染

1,236 阅读5分钟

性能优化是一个很大的话题,我们从 React Function 组件的性能优化中发散一下思维,精细化渲染指得是让每个渲染的粒度更细,让该渲染的部分渲染,不必要渲染的部分缓存,

setState

React 从 一次 SetState 到界面更新大致经过这些步骤:

调用 SetState(更新State) => render Function(组件render,函数执行) => diff(对比Vdom差异) => commit => render Dom(更新界面)

每次 render 并不一定会造成 页面 UI 的更新,其中会经过 diff 的优化

我们主要说说如何减少不必要的 render Function,减少不必要的组件函数吊用。

欲善其事必利其器

  1. 首先安装 react devtools

  2. 在 Components-setting-General 中打开 Highlight updates when components render.

    这样你就能看到哪些组件在 setState 后 render 了

  3. 在 Components-setting-General Profiling 中打开 Record why each component rendered while profiling.

    这样你就能知道是什么导致组件重新 render 了

列表渲染举例

我们以一个常见的列表渲染为例,我们想通过点击一个按钮更新列表第一项的 num

我们可能会写出如下代码

const listData = [
  { id: 'id-1', num: 1 },
  { id: 'id-2', num: 2 }
]

export const List = () => {
  const [list, setList] = useState(listData)
  
  const handleUpdateFirstItem = () => {
    const newList = [...list]
    newList[0] = { ...newList[0], num: Math.random() }
    // newList[0].num = Math.random() // 这样写永远都是是错误的,即使在这里写,最后页面显示结果也是正确的. react 不可变数据 原则了解一下
    setList(newList)
  }

  return (
    <ul>
      {list.map((item) => (
        <li key={item.id}>Num : {item.num} {console.log(`renderItemId: ${item.id}`)}</li>
      ))}
      <button onClick={handleUpdateFirstItem}>修改第一项</button>
    </ul>
  )
}

Mar-13-2021 22-39-04.gif

点击按钮,我们可以看到 renderItemIdid-1 id-2都打印了,但是很明显第二项是可以不需要render的,那该怎么做呢。

精细化列表渲染 + memo 缓存组件

把 每个 li 抽离成组件 Item 组件, 并memomemo 作用是和 React.PureComponent 一样,只不过是用在函数组件中,会对 propsstate浅比较。如果未发生变化,组件则不会更新。

export const List = () => {
  const [list, setList] = useState(listData)

  const handleUpdateFirstItem = () => {
    const newList = [...list]
    newList[0] = { ...newList[0], num: Math.random() }
    // newList[0].num = Math.random() // 如果这样写,子组件就不更新了,想想为什么,所以说 react 不可变数据 原则继续了解一下
    setList(newList)
  }

  return (
    <ul>
      {list.map((item) => (
        <Item key={item.id} item={item}/>
      ))}
      <button onClick={handleUpdateFirstItem}>修改第一项</button>
    </ul>
  )
}


const Item = React.memo(({ item }) => {
  console.log('renderItemId: ' + item.id)
  return (
    <li>
      {item.num}
    </li>
  )
})

Mar-15-2021 21-46-20.gif 点击按钮,我们可以看到 renderItemId 只有的 id-1 打印了,看到这里,需要记住:函数组件的 memo 和 class 组件的 React.PureComponent,是性能优化的好帮手。

我们需要尽可能的保证传入每个 Item 组件的 props 不会发生变化。例如:想知道当前 Item 是否是被选中,应该在 List 组件上做判断,而不是在 Item 组件里判断。 Item 只有 isActive props, 而不是把 整个 activeIdList 传入每个 Item 跟其 id 做比较,因为 activeIdList prop 的更新会导致每个 Item 都会 render,而 props 只接收isActive,只会在值真正变化的时候render Item.

有 Event 传递 如何优化

还是常见的需求,我们在上面列表的基础上,想点击某一项就更新某一项的 num

我们可能会有这些方式去实现:

方式一:把 list 传入每个 Item (极其不推荐)

export const List = () => {
  const [list, setList] = useState(listData)
  return (
    <ul>
      {list.map((item) => (
        <Item setList={setList} list={list} key={item.id} item={item}/>
      ))}
    </ul>
  )
}

const Item = React.memo(({ item, setList, list }) => {
  const handleClick = () => {
    const newList = [...list]
    const index = newList.findIndex((s) => s.id === item.id)
    newList[index] = { ...newList[index], num: Math.random() }
    setList(newList)
  }

  console.log('renderItemId: ' + item.id)

  return (
    <li>
      {item.num}
      <button onClick={handleClick}>点击</button>
    </li>
  )
})

Mar-16-2021 22-21-12.gif 为啥极其不推荐?我们发现其实仅只需要重新 render 当前项,但是其他 Item 也会更新。

Mar-16-2021 22-25-01.gif

通过 react devtools 我们可以看到每一项 Item 的 props 中的 list 导致重新 render

方式二:更新函数写在父组件,并且用 useCallback 缓存函数 无法缓存组件

export const List = () => {
  const [list, setList] = useState(listData)

  const handleChange = useCallback((id) => {
    const newList = [...list]
    const index = newList.findIndex((item) => item.id === id)
    newList[index] = { ...newList[index], num: Math.random() }

    setList(newList)
  }, [list])

  return (
    <ul>
      {list.map((item) => (
        <Item setList={setList} onClick={handleChange} key={item.id} item={item}/>
      ))}
    </ul>
  )
}

const Item = React.memo(({ item, onClick }) => {

  const handleClick = useCallback(() => {
    onClick(item.id)
  }, [item.id, onClick])

  console.log('renderItemId: ' + item.id)

  return (
    <li>
      {item.num}
      <button onClick={handleClick}>点击</button>
    </li>
  )
})

Mar-16-2021 22-34-58.gif 这样两个 Item 还是都重新 render 了,从分析工具中看到 props 中的 onClick 函数change了,因为 handleChange 即使 使用了 useCallback 缓存,但是由于必须依赖 list 但是每次都会重新 setList 导致每次传入的 handleChange 也是新创建的,破坏了meno 的效果。

方式三:改进方式二缓存 list

方式2就是由于 handleChange 依赖了 list,导致函数每次都会创建,我们想办法用 ref 缓存一下。

export const List = () => {
  const [list, setList] = useState(listData)

  // 用 ref 缓存 list
  const ref = useRef(list)

  // 监听 list 变化存到ref
  useEffect(() => {
    ref.current = list
  }, [ref, list])

  const handleChange = useCallback((id) => {
    const newList = [...ref.current]
    const index = newList.findIndex((item) => item.id === id)
    newList[index] = { ...newList[index], num: Math.random() }

    setList(newList)
  }, [ref]) // deps 依赖ref 而不依赖 list

  return (
    <ul>
      {list.map((item) => (
        <Item setList={setList} onClick={handleChange} key={item.id} item={item}/>
      ))}
    </ul>
  )
}
const Item = React.memo(({ item, onClick }) => {
  ...
})

Mar-16-2021 22-50-01.gif 这样就可以实现点击哪一项就只 render 哪一项。但是这样写每次需要c实在有点麻烦。

方式四:useEventCallBack (推荐方式)

方式3 用起来有点麻烦,可以自定义一个 useEventCallBack hook, React 官方有给出,自己写一个也行,这样就简单多了。

export const List = () => {
  // ...
  const handleChange = useEventCallBack((id) => {
    ...
  },[list])
  return (
    // ...
  )
}

方式五:利用 useReducer + useContext (多层数据传递推荐方式)

该方法适用于多层级的组件结构,暂不多说。

总结

总的来说就一句话,尽可能让只需要重新渲染的组件重新渲染。

回到本文的情景就是:

一般情况下:尽量让每个组件拆得粒度更细,让组件 memo 缓存。让组件的 props 尽可能的不变化。但是某些场景 一定最造成组件 render 的情景下,反复的 memo 浅比价也会产生开销,所以具体情况需要根据业务场景来做处理。

手动优化时:手动优化一般都是根据具体业务场景,去比较 props,有时候需要比较的 props 较多可以用 lodashpick,omit等方法取需要比较等字段,然后用 isEqual 进行值的比较。 需要注意到,这些取值,和比较计算也会有开销,所以还是需要根据实际业务场景进行取舍权衡

参考文档

Optimizing Performance React 官方文档