最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • React 性能优化实践 - 精细化渲染

    正文概述 掘金(AKing)   2021-03-21   763

    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>
      )
    }
    

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

    点击按钮,我们可以看到 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>
      )
    })
    

    React 性能优化实践 - 精细化渲染 点击按钮,我们可以看到 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>
      )
    })
    

    React 性能优化实践 - 精细化渲染 为啥极其不推荐?我们发现其实仅只需要重新 render 当前项,但是其他 Item 也会更新。

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

    通过 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>
      )
    })
    

    React 性能优化实践 - 精细化渲染 这样两个 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 }) => {
      ...
    })
    

    React 性能优化实践 - 精细化渲染 这样就可以实现点击哪一项就只 render 哪一项。但是这样写每次需要c实在有点麻烦。

    方式四:useEventCallBack (推荐方式)

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

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

    方式五:利用 memo or shouldComponentUpdate 手动优化 (推荐方式)

    这里实际的业务情景是,只有 num 发生变化的时候才需要 render,所以利用 memo 函数的第二个参数,只比较 props.num 就足够了。

    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>
      )
    }
    
    
    // 1. Function组件写法,用 memo 第二个参数,如果相等返回 true 则表示 不需要 `render`
    const Item = React.memo(({ item, onClick }) => {
    
      const handleClick = useCallback(() => {
        onClick(item.id)
      }, [item.id, onClick])
    
      return <li>
        {item.num}
        <button onClick={handleClick}>点击</button>
      </li>
    
    }, (preProps, nextProps) => {
      return preProps.item.num === nextProps.item.num
    })
    
    
    // 2. Class 组件,在 shouldComponentUpdate 生命周期函数上,对比 props,如果相等返回 true 则表示 需要 `render`
    shouldComponentUpdate(nextProps, nextState) {
      return nextProps.item.num !== this.props.item.num
    }
    
    

    总结

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

    回到本文的情景就是:

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

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

    参考文档

    Optimizing Performance React 官方文档


    起源地下载网 » React 性能优化实践 - 精细化渲染

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元