性能优化是一个很大的话题,我们从 React Function 组件的性能优化中发散一下思维,精细化渲染指得是让每个渲染的粒度更细,让该渲染的部分渲染,不必要渲染的部分缓存,
setState
React 从 一次 SetState 到界面更新大致经过这些步骤:
调用 SetState(更新State) => render Function(组件render,函数执行) => diff(对比Vdom差异) => commit => render Dom(更新界面)
每次 render 并不一定会造成 页面 UI 的更新,其中会经过 diff 的优化
我们主要说说如何减少不必要的 render Function,减少不必要的组件函数吊用。
欲善其事必利其器
-
首先安装 react devtools
-
在 Components-setting-General 中打开 Highlight updates when components render.
这样你就能看到哪些组件在 setState 后 render 了
-
在 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>
)
}
点击按钮,我们可以看到 renderItemId 的 id-1 id-2都打印了,但是很明显第二项是可以不需要render的,那该怎么做呢。
精细化列表渲染 + memo 缓存组件
把 每个 li 抽离成组件 Item 组件, 并memo,memo 作用是和 React.PureComponent 一样,只不过是用在函数组件中,会对 props 和 state 作 浅比较。如果未发生变化,组件则不会更新。
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>
)
})
点击按钮,我们可以看到
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>
)
})
为啥极其不推荐?我们发现其实仅只需要重新
render 当前项,但是其他 Item 也会更新。
通过 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>
)
})
这样两个
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 }) => {
...
})
这样就可以实现点击哪一项就只
render 哪一项。但是这样写每次需要c实在有点麻烦。
方式四:useEventCallBack (推荐方式)
方式3 用起来有点麻烦,可以自定义一个 useEventCallBack hook, React 官方有给出,自己写一个也行,这样就简单多了。
export const List = () => {
// ...
const handleChange = useEventCallBack((id) => {
...
},[list])
return (
// ...
)
}
方式五:利用 useReducer + useContext (多层数据传递推荐方式)
该方法适用于多层级的组件结构,暂不多说。
总结
总的来说就一句话,尽可能让只需要重新渲染的组件重新渲染。
回到本文的情景就是:
一般情况下:尽量让每个组件拆得粒度更细,让组件 memo 缓存。让组件的 props 尽可能的不变化。但是某些场景 一定最造成组件 render 的情景下,反复的 memo 浅比价也会产生开销,所以具体情况需要根据业务场景来做处理。
手动优化时:手动优化一般都是根据具体业务场景,去比较 props,有时候需要比较的 props 较多可以用 lodash 的 pick,omit等方法取需要比较等字段,然后用 isEqual 进行值的比较。 需要注意到,这些取值,和比较计算也会有开销,所以还是需要根据实际业务场景进行取舍权衡
参考文档
Optimizing Performance React 官方文档