React 重新渲染(re-render)
🍰1. React 的 re-render
是什么
探讨 React 性能问题时,一般主要关注两个内容
initial render
(初始渲染),即组件首次出现在屏幕上re-render
(重新渲染),即对已出现在屏幕上的组件进行第二次或者任何连续的渲染
当 React 需要使用新的数据去更新应用程序的时候,就会发生 re-render
. 通常是由于用户与应用程序进行交互,或通过异步请求或某种订阅模型获取某些外部数据
没有异步数据更新或者非交互式应用程序永远不会发生 re-render
🍀2. 必要的 re-render
和 不必要的 re-render
🍃2.1 必要的 re-render
重新渲染更改源组件或一个组件直接使用新的信息。如用户在 input 输入框中输入数据,管理状态的组件需要在每次按键时更新
🍂2.2 不必要的 re-render
由于错误或低效的应用程序架构,通过不同的渲染机制在应用程序中传递的组件的重新渲染。如用户键入 input 输入框的场景,整个页面在用户发生按键动作时都会重新渲染,这里整个页面就是不必要的重新渲染
尽管 React 的速度很快,通常可以在用户无知觉的状况下处理,但是如果重新渲染发生得过于频繁或者发生在非常重的组件上,就可能会导致用户体验滞后,即交互延迟或者应用程序没有反应
🕛3. re-render
时机
组件 re-render
的四个场景
state
改变- 父/子组件
re-render
context
改变hooks
改变
还有一个是组件的 props
变化
💭3.1 state
改变
组件 state
发生变化时,会 re-render
. 通常会发生在 callback
或 useEffect
hook
state
改变是所有 re-render
的根源
💭3.2 parent re-render
如果一个组件的父组件重新渲染,该组件也会重新渲染。即一个组件重新渲染时,也会重新渲染它的所有子组件
大多数情况是沿着树向下渲染,即子组件的重新渲染不会触发父组件重新渲染
💭3.3 context
改变
context provider
值变化时,所有使用这个 context
组件都会 re-render
,即使他们没有直接使用数据的变化部分
// 1. value changes
const useValue = useContext(Context)
// 2. re-render
const Component = () => {
const value = useValue()
}
并且这部分重新渲染无法直接通过缓存来防止,但是可以通过使用高阶组件和 React.memo
来伪造上下文选择器
useMemo
无效例子
const useSomething = () => {
// 1. 会触发 re-render 即使 something 不改变
const {something} = useContext(Context)
// 2. useMemo 不会起作用
return useMemo(() => something, [something])
}
const Component = () => {
// 3. 会 re-render 即使 something 不改变
const {something} = useSomething()
return ...
}
HOC
+ React.memo
伪造上下文选择器
const withSomething = (Component) => {
// 1. 组件被缓存
const MemoComponent = React.memo(Component)
return () => {
const { something } = useSomething()
// 2. 只有在 something 改变时才会 re-render
return <MemoComponent something={something} />
}
}
// 3. 只有在 something 改变时才会 re-render
const Component = withSomething(({something}) => {
return ...
})
💭3.4 hooks
改变
hook
中发生的所有事情都属于使用它的组件,同样适用于 context
和 state
改变的规则
hook
内state
的改变会触发宿主组件不可预防的re-render
- 如果
hook
使用了context
,并且context
的值改变,就会触发宿主组件不可预防的re-render
hook
可以被链式使用,链条中每一个 hook
仍属于宿主组件,并且同样的规则适用于任何一个 hook
// 1. value 改变
const useSomething = useContext(Context)
// 2. chain reaction
const useValue = {
useSomething()
}
// -----------------------
// 3. re-renders
const Component = () => {
const value = useValue()
return ...
}
💬 3.5 props
改变
在 re-render
非缓存组件时,组件的 props
是否改变并不重要
为了改变 props
,需要通过父组件对其进行更新。这意味着父组件需要 re-render
,这会触发子组件的 re-render
,不管 props
是什么
只有在使用 React.memo
or useMemo
时,props
改变才重要
🚑4. 规避 re-render
的方式
🌟4.1 巧妙利用组合
⛔4.1.1 避免在 render function
中创建组件
在另一个组件的 render function
中创建组件是一种反模式,这可能是最大的性能杀手
每次 re-render
react 会 re-mount
该组件(即 destroy
销毁它并从头开始 re-create
重新创建),这会比正常的 re-render
慢更多,除此之外还会导致一些问题:
re-render
时内容可能闪烁- 每次
re-render
都会在组件中reset
重置state
- 每次
re-render
都触发无依赖的useEffect
- 如果某个组件已经
focused
,focus
将会失去
反模式:
// 1. re-render
const Component = () => {
// 2. new component
const SlowComponent = () => {
console.log("slow component re-renders");
useEffect(() => {
console.log("slow component re-mounts");
}, []);
return <div>slow component</div>;
};
return (
// 3. re-mount
<SlowComponent/>
)
}
放到 render function
外
// 2. same component
const SlowComponent = () => {
console.log("slow component re-renders");
useEffect(() => {
console.log("slow component re-mounts");
}, []);
return <div>slow component</div>;
};
// 1. re-render
const Component = () => {
return (
// 3. 仅 re-render,不触发无依赖的 useEffect
<SlowComponent/>
)
}
✅4.1.2 向下移动 state
当一个重型组件需要管理 state
,并且 state
只用于 render tree
的一小部分时,这个方式会很棒
典型场景就是在一个复杂的组件中通过点击按钮打开/关闭对话框,而改组件会渲染页面大部分内容
在这种情况下,控制对话框显隐状态、对话框本身和触发更新的按钮都可以封装在一个较小的组件中。这样较大的组件不会在这些 state
发生变化时 re-render
const SlowComponent = () => {
console.log('slow component re-renders')
return <>slow component</>
}
const FullComponent = () => {
const [isShow, setIsShow] = useState(false)
const handleClick = () => {
// 1. state change,触发 re-render
setIsShow(!isShow)
}
return (
<div>
<button onClick={() => handleClick()}>click here</button>
{isShow && <div>dialog</div>}
{/* 2. re-render */}
<SlowComponent />
</div>
);
}
const SlowComponent = () => {
console.log('slow component re-renders')
return <>slow component</>
}
const ComponentWithButton = () => {
const [isShow, setIsShow] = useState(false)
const handleClick = () => {
// 1. state change,触发 re-render
setIsShow(!isShow)
}
return (
<div>
<button onClick={() => handleClick()}>click here</button>
{isShow && <div>dialog</div>}
</div>
)
}
const SplitComponent = () => {
return (
<div>
<ComponentWithButton/>
{/* 2. 不受影响 */}
<SlowComponent />
</div>
);
}
✅4.1.3 children
as props
这种模式和前面的“向下移动”类似,那种将 state
包裹在 children
周围的感觉,即将 state
变化封装在一个较小的组件中
区别是 state
用在一个元素上,该元素包裹了 render tree
的一个较慢的部分,所有无法轻松提取
较多的使用场景是 onScroll
或 onMouseMove
的 回调 callbacks
附加到组件的根元素上
这种情况 state
管理和使用相关 state
的组件提取到一个小组件中,并将慢组件作为 children
传递给它。从较小组件的角度看,children
只是 prop
,不会收到 state
改变的影响,故不会 re-render
const SlowComponent = () => {
console.log("slow component re-renders")
return <div>slow component</div>
}
const FullComponent = () => {
const [state, setState] = useState(0)
const handleClick = () => {
// 1. 触发 re-render
setState(state + 1)
}
return (
<div onClick={() => handleClick()}>
<div>Re-render count: {state}</div>
{/* 2. re-render */}
<SlowComponent />
</div>
)
}
const SlowComponent = () => {
console.log("slow component re-renders")
return <div>slow component</div>
}
const ComponentWithClick = ({children}) => {
const [state, setState] = useState(0)
const handleClick = () => {
// 1. 触发 re-render
setState(state + 1)
}
return (
<div onClick={() => handleClick()}>
<div>Re-render count: {state}</div>
{/* 2. props,不受影响 */}
{children}
</div>
)
}
const SplitComponent = () => {
return (
<ComponentWithClick>
{/* 3. 不受影响 */}
<SlowComponent/>
</ComponentWithClick>
)
}
✅4.1.4 components
as props
和之前的 children
作为 props
类似,将 state
封装在一个较小的组件内,重型组件作为 props
传递,props
不受 state
改变的影响,故重型组件不会 re-render
当一些重型组件的 state
独立,但是无法作为 children
提取出来时,这种方法就很棒
const SlowComponent = () => {
console.log("slow component re-renders")
return <div>slow component</div>
}
const AnotherSlowComponent = () => {
console.log("another slow component re-renders")
return <div>another slow component</div>
}
const FullComponent = () => {
const [state, setState] = useState(0)
const handleClick = () => {
// 1. 触发 re-render
setState(state + 1)
}
return (
<div>
<div>Re-render count: {state} </div>
{/* 2. re-render */}
<SlowComponent/>
{/* 2. re-render */}
<AnotherSlowComponent/>
</div>
)
}
const SlowComponent = () => {
console.log("slow component re-renders")
return <div>slow component</div>
}
const AnotherSlowComponent = () => {
console.log("another slow component re-renders")
return <div>another slow component</div>
}
const ComponentWithClick = (left, right) => {
const [state, setState] = useState(0)
const handleClick = () => {
// 1. 触发 re-render
setState(state + 1)
}
return (
<div>
<div>Re-render count: {state} </div>
{/* 2. 作为 props 不受影响 */}
{left}
{/* 2. 作为 props 不受影响 */}
{right}
</div>
)
}
const SplitComponent = () => {
const left = <SlowComponent/>
const right = <AnotherSlowComponent/>
return (
<>
<ComponentWithClick left={left} right={right}/>
</>
)
}
🛴4.2 使用 React.memo
使用 React.memo
包裹组件可以停止在 render tree
的某处触发下游的 re-render
链条,除非组件的 props
改变
在渲染不依赖 re-render
源头(state
改变)的重型组件的场景这个方法很棒
const Child = () => {
console.log("child re-render")
return <>child</>
}
const ChildMemo = React.memo(Child)
const Component = () => {
const [state, setState] = useState(0)
const handleClick = () => {
setState(state + 1)
}
return (
<>
<button onClick={() => handleClick()}>click here, state: {state}</button>
<ChildMemo/>
</>
)
}
✅4.2.1 React.memo
+ component with props
所有的非基础类型(引用类型)的 props
都必须进行缓存(useMemo
),这样 React.memo
才能工作
const Child = ({value}) => {
console.log('Child re-render ', value.value)
return <>{value.value}</>
}
const ChildMemo = React.memo(Child)
const Component = () => {
const [state, setState] = useState(0)
const handleClick = () => {
// 1. 触发 re-render
setState(state + 1)
}
const memoValue = useMemo(() => ({value: 'second'}), [])
return (
<>
<button onClick={()=>handleClick()}>click here, state: {state}</button>
{/* 2. value changes, re-render */}
<ChildMemo value={{value: 'first'}}/>
{/* 2. memoValue 没变, 不会 re-render */}
<ChildMemo value={memoValue}/>
</>
)
}
✅4.2.2 React.memo
+ components as props or children
React.memo
必须应用于作为 children
或 props
的元素
当 children
和 props
都是 object
时,每次 re-render
都会改变,缓存的父组件将会不起作用
const Child = ({value}) => {
console.log("Child re-render ", value.value)
return <>{value.value}</>
}
const ChildMemo = React.memo(Child)
const Parent = ({left, children}) => {
return (
<div>
{left}
{children}
</div>
)
}
const ParentMemo = React.memo(Parent)
const Component = () => {
const [state, setState] = useState(0)
const handleClick = () => {
// 1. 触发 re-render
setState(state + 1)
}
const memoValue = useMemo(() => ({value: "memoized"}), [])
return (
<>
<button onClick={() => handleClick()}>click here,state: {state}</button>
{/* re-render */}
<ParentMemo left={<child value={{value: "left child of ParentMemo"}} />}>
<Child value={{value: "child of ParentMemo"}}/>
</ParentMemo>
{/* re-render */}
<ParentMemo left={<ChildMemo value={{value: "left ChildMemo of ParentMemo without memoValue"}} />}>
<ChildMemo value={{value: "ChildMemo of ParentMemo without memoValue"}}/>
</ParentMemo>
{/* doesn't re-render */}
<Parent left={<ChildMemo value={memoValue} />}>
<ChildMemo value={memoValue}/>
</Parent>
</>
)
}
🚗4.3 使用 useMemo
/useCallback
提高性能
⛔4.3.1 反模式:在 props
中使用非必要的 useMemo
/useCallback
缓存 props
本身不会阻止子组件 re-render
,如果一个父组件 re-render
,无论 props
怎么样都会触发它子组件的 re-render
const Child = ({value}) => {
console.log("child re-render", value.value)
return <>{value.value}</>
}
const Component = () => {
const [state, setState] = useState(0)
const handleClick = () => {
// 1. 触发 re-render
setState(state + 1)
}
const memoValue = useMemo(() => ({value: "child"}), [])
return (
<>
<button onClick={()=>handleClick()}>click here, state:{state}</button>
{/* 2. re-render */}
<Child value={memoValue} />
</>
)
}
✅4.3.2 必要的使用 useMemo
/useCallback
如果子组件被 React.memo
包裹,则所有非基础类型(引用类型)的 props
都必须被缓存
const Child = ({value}) => {
console.log('Child re-render ', value.value)
return <>{value.value}</>
}
const ChildMemo = React.memo(Child)
const Component = () => {
const [state, setState] = useState(0)
const handleClick = () => {
// 1. 触发 re-render
setState(state + 1)
}
const memoValue = useMemo(() => ({value: 'second'}), [])
return (
<>
<button onClick={()=>handleClick()}>click here, state: {state}</button>
{/* 2. value changes, re-render */}
<ChildMemo value={{value: 'first'}}/>
{/* 2. memoValue 没变, 不会 re-render */}
<ChildMemo value={memoValue}/>
</>
)
}
如果组件在 useEffect
, useMemo
, useCallback
等 hook
中使用非基础类型值作为依赖,也应该被缓存
const component = () => {
const [state, setState] = useState(0)
const handleClick = () => {
// 1. 触发 re-render
setState(state + 1)
}
const value = {value: "not memoized"}
const memoValue = useMemo(() => ({ value: "memoized" }))
// 除首次渲染其他不触发
useEffect(() => {
console.log("never triggered")
}, [memoValue])
// 每次重新渲染都触发
useEffect(() => {
console.log("triggered on every re-render")
}, [value])
return (
<button onClick={() => handleClick()}>click here</button>
)
}
✅4.3.3 使用 useMemo
进行复杂的计算
useMemo
的一个用处是避免每次 re-render
是进行复杂的计算
useMemo
存在性能耗费问题(消耗部分内存,让初次渲染变慢),不能滥用。在 React 中,挂载和更新组件是最昂贵复杂的计算
所以,使用 useMemo
的典型场景是缓存 React 元素,通常是已经存在的 render tree
的一部分或生成 render tree
的结果,如返回新元素的 map 函数
同组件更新相比,排序或过滤数组这种纯 JavaScript 操作的成本可以忽略不计
const Child = ({value}) => {
console.log("child re-render", value.value)
return <>{value.value}</>
}
const values = [1,2,3]
const values2 = [4,5,6]
const Component = () => {
const [state, setState] = useState(0)
const handleClick = () => {
// 1. 触发 re-render
setState(state + 1)
}
const items = useMemo(() => {
return values.map((v,i) => <Child key={i} value={{v}} />)
}, [])
const items2 = values2.map((v,i) => <Child key={i} value={{v}} />
return (
<>
<button> click here {state} </button>
{items}
<br/>
{items2}
</>
)
}
const SlowComponent = () => {
console.log("slow component re-renders");
return <div>slow component</div>;
};
const component = () => {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
};
const slowComponent = useMemo(() => {
return <SlowComponent />;
}, []);
return (
<>
<button onClick={onClick}>click here {state}</button>
<br />
{/* 不会 re-render */}
{slowComponent}
</>
)
}
🚄5. 提高列表的 re-render
性能
除了上面那些和 re-render
相关的规则模式外,key
属性也会影响 React
列表中的性能
仅提供 key
属性并不能提高列表的性能,为了防止列表元素 re-render
,需要使用 React.memo
对其进行封装,并遵守所有的最佳实践😆
key
中的值需要是字符串,并且在列表中每次元素 re-render
时 key
都需要保持一致
通常 item
的 id
或数组的 index
可以用来当做 key
如果列表是静态的,即元素不会添加/删除/插入/重新排序,则可以使用数组的 index
作为 key
but,在动态列表中,使用数组 index
作为 key
就有问题了
- 有
state
或 不受控的元素(如 form inputs)的话,可能会出现错误 - 如果
items
被React.memo
包裹,性能会下降
const Child = ({ value }: { value: number }) => {
console.log("Child re-renders", value);
return <div>{value}</div>;
};
const values = [1,2,3]
const ChildMemo = React.memo(Child)
const Component = () => {
const [state, setState] = useState(0)
const handleClick = () => {
setState(state + 1)
}
return (
<>
<button onClick={() => handleClick()}>click here {state}</button>
<br/>
{values.map((val,idx) => (<ChildMemo value={val} key={idx} />))}
<br/>
{values.map((val) => (<ChildMemo value={val} key={val} />))}
</>
)
}
const Child = ({ value }: { value: number }) => {
console.log("Child re-renders", value);
return <div>{value}</div>;
};
const values = [1,2,3]
const ChildMemo = React.memo(Child)
const Component = () => {
const [state, setState] = useState(false)
const handleClick = () => {
setState(!state)
}
const sortedValues = state ? values.sort() : values.sort().reverse()
return (
<>
<button onClick={() => handleClick()}>click here {state}</button>
<br/>
{/* 用 React.memo 包裹也 gg,还是触发了 re-render */}
{sortedValues.map((val, index) => (<ChildMemo value={`child of index: ${val}` key={index}} />))}
<br/>
{/* 没触发 re-render */}
{sortedValues.map((val) => (<ChildMemo value={`child of val: ${val}` key={val}} />))}
</>
)
}
⛔5.1 反模式:使用随机数作为列表的 key
这个是必需必需必需避免的行为,打死也不能用随机数作为列表的 key
,因为会导致 React 在每次 re-render 时都 re-mount
元素,进而
- 列表性能糟糕
- 有
state
或 任何不受控元素(如 form inputs)时出现错误
const Child = ({ value }: { value: number }) => {
console.log("Child re-renders", value);
useEffect(() => {
console.log("Child re-mounts");
}, []);
return <div>{value}</div>;
};
const values = [1,2,3]
const ChildMemo = React.memo(Child)
const Component = () => {
const [state, setState] = useState(0)
const handleClick = () => {
setState(state + 1)
}
return (
<>
<button onClick={() => handleClick()}>click here {state}</button>
<br/>
{/* 导致 re-mounts every render ! */}
{values.map((val) => (<ChildMemo value={val} key={Math.random()} />))}
</>
)
}