Experience in performance optimization in React applications (1)

Original link: https://innei.ren/posts/programming/experience-in-performance-optimization-in-react-applications-(1)

This rendering is generated by marked, and there may be typographical problems. For the best experience, please go to: https://innei.ren/posts/programming/experience-in-performance-optimization-in-react-applications-(1)

Before reading, it is recommended to read react-re-renders-guide first. After understanding the theoretical knowledge, you can have a deep understanding through actual business scenarios.

Make good use of React Devtool and Profiler

There is a very useful feature in React devtool called Highlight updates when components render. Use this function to highlight the re-render component. A re-render will be highlighted with a green frame. If it is a yellow frame, it has caused multiple re-renders in a short period of time. You need to consider which component has performance problems.

0527141154.png

0527222057.gif

This tool can quickly locate which component on the page has a problem, but it cannot locate the specific problem.

We know that the re-rendering of React components must be caused by a certain hook, and the key is how to find frequently updated hooks. At this point we need to use React Profiler to troubleshoot.

Switch to the Profiler Tab, click Record on the upper left, and trigger the re-render operation, you can see the following page:

0527221602.png

As shown above, NodeCards is a list component that loops through the NodeCard component. When the mouse hovers over a NodeCard, it causes re-rendering of the parent component NodeCards, causing re-rendering of all child components. This performance overhead is relatively large, and you should always pay attention to it in long list components. And move the whole body.

Through Profiler, we found that the Hook 136 of NodeCards has changed and caused this re-render. But we don’t know what the cause of 136 is (criticizing React devtool). We need to switch back to Components at this time, and we can see that the right panel of the corresponding component has the serial number corresponding to the Hook, and the corresponding Hook can be touched through the serial number. Of course, most Hooks are combined with multiple other hooks, so we can only make a general judgment.

0527223523.gif

For example, in the picture above, we can judge that Hook No. 136 is actually a Hook of Jotai.

How List Components Should Avoid Performance Issues

Use Memo?

In the example above, updating one component in the list causes the entire list to update, which is very green. Through a few frames of the Profiler, it is found that most of the NodeCards themselves have not been updated, but the upper NodeCards have been updated. At this time, we can use memo to wrap a layer on NodeCard. This way parent updates don’t cause the list elements to all be updated. In the list (List) scene, it is generally recommended to wrap the memo on the list element (ListItem). It is very beneficial to use this space to exchange time for long lists, unless you ensure that the state of the list will not be updated.

 const NodeCards = () => { // some hooks return <> {data.map(item => <NodeCard data={item} />)} </> } const NodeCard = memo((props) => { return


})

Pass in id instead of the datasource?

Generally, we can directly pass in the data source when traversing the list data, as shown above. But due to the immutable feature of React, if the data source changes, your component will definitely update, even if the specific value of the data source change is not used in the ListItem. As an example:

 const dataMapAtom = atom({ '1': { name: 'foo', desc: '', id: '1' } // others.. }) const NodeCards = () => { const [data, setData] = useAtom(dataMapAtom) // we update data[0].desc useEffect(() => { setData((data) => ({ ...data, '1': { ...data['1'], desc: 'bar' } })) }, []) return <> {data.map(item => <NodeCard data={item} />)} </> } const NodeCard = memo((props) => { const { name } = props.data return
{name}


})

Even though NodeCard does not consume desc , it will be re-rendered once the data changes. This is something we don’t want to see. But if we pass in the id, and then cooperate with the selector, there will be no such unfriendly behavior.

 const dataMapAtom = atom({ '1': { name: 'foo', desc: '', id: '1' } // others.. }) const NodeCards = () => { const [data, setData] = useAtom(dataMapAtom) // maybe some state change and hook update here. // we update data[0].desc useEffect(() => { setData((data) => ({ ...data, '1': { ...data['1'], desc: 'bar' } })) }, []) return <> {data.map(item => <NodeCard id={item.id} />)} </> } const NodeCard = memo((props) => { const { id } = props const name = useAtomValue( selectAtom( dataMapAtom, useCallback((dataMap) => dataMap.id.name, []) ) ) return
{name}


})

Where should the state of the list elements be maintained?

Maybe we will encounter in the list, we need to display different UI expressions according to a certain state. For example, when a card is hovering, I need to do some motion or other scenes that cannot be done simply through CSS. for example:

 const NodeCards = () => { const [activeId, setActiveId] = useState(0) // some hooks return <> {data.map(item => <NodeCard data={item} activeId={activeId} setActiveId={setActiveId} />)} </> } const NodeCard = memo((props) => { // do thing. return
{
props.setActiveId(props.data.id)
}} />
})

以上代码是非常非常错误的。在列表中你不应该把这些动态可变值直接传入到列表元素。举个例子,上面的写法就会导致鼠标hover 到一个NodeCard 时,所有NodeCard 也被re-render。


正确的应该是传入一个布尔值,在NodeCards 就完成判断:


 const NodeCards = () => { const [activeId, setActiveId] = useState(0) // some hooks return <> {data.map(item => <NodeCard data={item} isActive={activeId === item.id} setActiveId={setActiveId} />)} </> } const NodeCard = memo((props) => { // do thing. return
{
props.setActiveId(props.data.id)
}} />
})

上面修改为isActive则不会出现这样的问题。


再者有说了,如果我就是要拿到activeId但是NodeCard 又是一个ExpensiveComponent 怎么办呢。方法一是我们可以使用Ref,但是Ref 是脱离响应式数据流的。方法二借助Jotai 类似的状态库外置状态+ 拆分轻量组件。


 const activeIdAtom = atom(0) const NodeCards = () => { // some hooks return <> {data.map(item => <NodeCardExpensiveComponent data={item} />)} </> } const NodeCardExpensiveComponent = memo((props) => { const setActiveId = useSetAtom(activeIdAtom) // do thing. return <>
{
props.setActiveId(props.data.id)
}}
/>
{new Array(10000).fill(0).map(() =>

)}
<ObservedActiveIdHandler />
</>
})

const ObservedActiveIdHandler = () => {
const activeId = useAtomValue(activeIdAtom)
useEffect(() => {
// do thing.
}, [activeId])
}

我们把activeId的响应式处理拆分到轻量组件就不会影响开销很大的组件就会非常环保。


这种方法在其他地方的优化一样适用。


从Performance 火焰图发现性能问题


Chrome devtools 有个很好用的火焰图工具。在页面空载但是CPU 大量使用,并且React devtool 没有任何高亮组件的re-render 的时候,就需要用到它了。


0527232343.png


通过截取一段火焰图可以排查到短时间内CPU 占用过高的问题。上图所示的火焰图时间切片非常密集,在空闲的情况下不应该出现这样的JS 调用量,就可以使用Call Tree 进行进一步的排查。


0527232922.png


Timer Fired 也就是计时器导致的问题,顺着这个思路去找,最后就发现了同事写了错误的写了一个delay 为0 的setInterval。


以上就是今天的一些分享,计划之后再写写Jotai & Zustand 应该如何使用,粒度化组件和父组件状态下沉到子等一些经验。


看完了?说点什么呢

This article is transferred from: https://innei.ren/posts/programming/experience-in-performance-optimization-in-react-applications-(1)
This site is only for collection, and the copyright belongs to the original author.