🌿 性能优化的主要点: 1️⃣ 减少 DOM 的渲染频次 2️⃣ 减少 DOM 的渲染范围 3️⃣ 非必要的内容延后处理
React
在组件触发刷新的时候,会深度遍历所有子组件。➡️ 父组件刷新,子组件跟着刷新。
避免不必要的组件重新渲染,是提高程序性能的重要方式之一。
缓存方式
React.memo
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
使用 memo
将组件包装起来,以获得该组件的一个 记忆化 版本。通常情况下,只要该组件的 props 没有改变,这个记忆化版本就不会在其父组件重新渲染时重新渲染。
❗即使一个组件被记忆化了,当它自身的状态/ context 发生变化时,它仍然会重新渲染。memoization 只与从父组件传递给组件的 props 有关。
const MyComponent = memo(({name}) => {
const [age, setAge] = useState();
return (<>
<input type="text" value={age} onChange={(e) => setAge(e.target.value)}/>
{Date.now()}
</>)
});
① props name
改变,及 ② 自身 state age
改变,组件都会重新渲染。
当使用 memo
时,只要任何一个 prop 与先前的值不等的话,组件就会重新渲染。这意味着 React 会使用 Object.is
比较组件中的每个 prop 与其先前的值。
Object.is(3, 3) // true
Object.is({}, {}) // false
Object.is()
与 ==
运算符并不等价。==
运算符在测试相等性之前,会对两个操作数进行类型转换(如果它们不是相同的类型)。
"" == false // true
Object.is("", false) // false
Object.is()
与 ===
运算符也不等价。Object.is()
和 ===
之间的唯一区别在于它们处理带符号的 0 和 NaN
值的时候。
+0 === -0 // true
0 === NaN // false
Object.is(-0, +0) // false
Object.is(0, NaN) // false
当props是对象或函数时,Object.is()
永远不相等。为了解决这个问题,React 中引入了 useMemo
及 useCallback
。
👉 👉 如果 props 是一个对象,可以使用 useMemo
避免父组件每次都重新创建该对象。
useMemo
const cachedValue = useMemo(calculateValue, dependencies)
在每次重新渲染的时候能够缓存计算的结果。如果依赖项没有发生改变,它将返回上次缓存的值;否则将再次调用 calculateValue
,并返回最新结果。
function Page() {
const [name, setName] = useState('ligang');
const [age, setAge] = useState(34);
// 避免因 `Object.is` 不相等,导致的重新渲染
const person = useMemo(
() => ({ name, age }),
[name, age]
);
return <MyComponent person={person} />;
}
const MyComponent = memo(({person}) => {
// ...
});
👉👉 如果 props 是一个函数,可以将函数包装到 useCallback
避免父组件每次都重新创建该函数。
useCallback
const cachedFn = useCallback(fn, dependencies)
在多次渲染中缓存函数。在初次渲染时,useCallback
返回传入的 fn
函数;在之后的渲染中,如果依赖没有改变,useCallback
返回上一次渲染中缓存的 fn
函数;否则返回这一次渲染传入的 fn
。
export default () => {
const [name, setName] = useState('');
// 不依赖任何state
const handleSubmit = useCallback(() => {
// ...
}, []);
return MyComponent onSubmit={handleSubmit}/>;
}
const MyComponent = memo(({onSubmit}) => {
// ...
});
【示例】
不使用 useCallback
,每次父组件 name
改变,子组件MyComponent
都重新渲染(即便使用 memo
进行了包裹)
使用 useCallback
,每次父组件 name
改变,子组件MyComponent
不再重新渲染( memo
生效)
精准控制
key
key
是一个特殊的字符串属性,用于帮助 React 识别哪些元素改变了。
在列表渲染时 key
属性可以用于识别 React 的 diff
算法哪些列表项已更改,通过复用具有相同 key
的组件实例,React可以减少了不必要的DOM操作&重新渲染,从而提升界面更新的效率。
为了正确使用key属性,确保所提供的key是稳定、唯一且可预测的。
虚拟化
最常见的虚拟列表。仅渲染可见部分,而不是全部内容,实现性能的提升。
以 ahooks useVirtualList 为例:
export default () => {
const containerRef = useRef(null);
const wrapperRef = useRef(null);
const originalList = useMemo(() => Array.from(Array(99999).keys()), []);
// 配置虚拟滚动
const [list] = useVirtualList(originalList, {
containerTarget: containerRef, // 外面容器
wrapperTarget: wrapperRef, // 内部容器
itemHeight: 60, // 行高度
overscan: 10, // 视区上、下额外展示的 DOM 节点数量
});
return (
<>
{/* 指定可视区域高度 */}
<div ref={containerRef} style={{ height: '300px', overflow: 'auto', border: '1px solid' }}>
<div ref={wrapperRef}>
{list.map((ele) => (
<div
style={{
height: 52,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
border: '1px solid #e8e8e8',
marginBottom: 8,
}}
key={ele.index}
>
Row: {ele.data}
</div>
))}
</div>
</div>
</>
);
};
代码分割&懒加载
代码分隔(Code splitting)是一种前端技术,用于将应用程序的代码拆分成较小的块,以优化加载性能和减少初始加载时间。它可以帮助减少初始下载量,并根据需要动态加载所需的代码。
在需要时才加载组件,从而减少初始加载时间并提高性能。
动态导入(Dynamic Imports)
export default () => {
const [module, setModule] = useState(null);
useEffect(() => {
import('./MyModule').then((mod) => setModule(mod));
}, []);
return module ? <module.default /> : <div>Loading...</div>;
}
React.lazy()
和 Suspense
const MyModule = React.lazy(() => import('./MyModule'));
export default () => (
<Suspense fallback={<div>Loading...</div>}>
<MyModule />
</Suspense>
);