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

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

setState

React 从 一次 SetState 到界面更新大体通过这些步骤:html

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

每次 render 并不必定会形成 页面 UI 的更新,其中会通过 diff 的优化react

咱们主要说说如何减小没必要要的 render Function,减小没必要要的组件函数吊用。数组

欲善其事必利其器

  1. 首先安装 react devtools缓存

  2. 在 Components-setting-General 中打开 Highlight updates when components render.性能优化

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

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

    这样你就能知道是什么致使组件从新 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 官方文档