前言
性能优化是前端开发中绕不开的话题,在 React 开发中针对不同的应用场景的性能优化有着非常多共性和特性的方法,本文介绍了一些方法通过 React 原生支持的能力来对 React 应用程序进行性能优化,减少应用中无效的 re-render,不管你是新手小白还是成熟的 React 开发者都非常容易使用,并且可以应用到日常的开发中去。
优化 1 - 通过 Children 或 Props 传递元素
为了提高应用的性能,避免无效的 re-render 是非常实用的方法,想要避免无效 re-render,首先我们得了解组件什么时候会被 re-render。在 React 中,组件只会在三种情况下发生 re-render:State 发生改变、Context 发生改变和父组件 re-render。State 发生改变和 Context 发生改变比较好理解,而所谓父组件 re-render 指的是父组件 re-render 会导致所有子组件也 re-render,这里有一个非常常见的误解,即"当组件使用 Props 时,我们一般认为 Props 发生改变则组件会 re-render,但实际并不是这样,组件之所以会 re-render,是因为父组件 re-render 导致子组件 re-render 了,而 Props 是来自于父组件的"。注意,我们这里所说的 re-render 并不是指 Dom 重新渲染,这里的 re-render 指的是组件函数被调用执行,然后创建一个新的虚拟 Dom 去 diff 和更新,而无效的 re-render 指的就是 Dom 实际没有发生任何变化的渲染,执行的所有内容都是无用功。当 re-render 发生非常频繁或者组件本身执行就较为复杂时这种无效的 re-render 则会导致明显的性能问题。
typescript
import { useState } from "react";
function SlowComponent() {
const words = Array.from({ length: 100_000 }, () => "WORD");
return (
<ul>
{words.map((word, i) => (
<li key={i}>
{i}: {word}
</li>
))}
</ul>
);
}
export default function App() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Slow counter?!?</h1>
<button onClick={() => setCount((c) => c + 1)}>Increase: {count}</button>
<SlowComponent />
</div>
);
}
假设我们有一个计数组件 Counter,其中包含了一个渲染非常耗费资源的 SlowComponent(渲染了 10w 个内容),当我们点击计数按钮时,我们明显感受到计数值变化非常卡顿,并且从 profiler developer tool 录制的组件渲染情况中发现,SlowComponent 也 re-render 了。点击按钮会导致 State 发生改变,这时候 Counter 组件会触发 re-render,SlowComponent 组件作为 Counter 组件的子组件也会发生 re-render,但计数按钮点击并不会引起这个 SlowComponent 的变化,所以这是无效的 re-render。
typescript
import { useState } from "react";
function SlowComponent() {
const words = Array.from({ length: 100_000 }, () => "WORD");
return (
<ul>
{words.map((word, i) => (
<li key={i}>
{i}: {word}
</li>
))}
</ul>
);
}
function Counter({ children }) {
const [count, setCount] = useState(0);
return (
<div>
<h1>Slow counter?!?</h1>
<button onClick={() => setCount((c) => c + 1)}>Increase: {count}</button>
{children}
</div>
);
}
export default function App() {
return (
<Counter>
<SlowComponent />
</Counter>
);
}
父组件 re-render 会导致子组件 re-render,当某些复杂内容可以独立于父组件存在时或者我们并不需要 re-render 的话,我们可以通过 children 或 props 传入,这样就避免了无效的 re-render。如上所示,我们将 SlowComponentt 通过 children 传入 Counter 组件,当我们再次点击计数按钮时,通过 profiler developer tool 可以看出当 Counter 组件的 State 发生变化时,只有 Counter 组件进行 re-render,SlowComponent 没有触发 re-render,而不是每次 Counter 组件的 State 发生变化时都重新渲染,这是因为 SlowComponent 是 App 组件的子组件了,只有 App 组件发生 re-render 才会执行 re-render。
优化 2 - 使用 Memoization
所谓 Memoization(记忆化)就是只执行一次函数并记忆结果,当一个函数被执行的时候,记住执行得到的结果到缓存中,如果下次使用相同的输入,则直接返回缓存中记录的结果不再重新计算,如果是不同的输入,则会重新执行并计算结果。通过 Memoization 我们不仅可以避免无效的 re-render,还能够提高应用的速度和响应能力,Memoization 主要有三方面内容:
- Memoize Components使用memo函数
- Memoize Objects使用useMemo
- Memoize Fucntions 使用useCallback
Memoize component 组件可以通过 memo
函数实现,组件使用 memo 函数后,只要** props 保持不变**,那父组件 re-render 时就不会被影响触发 re-render,当然组件内部的 state 或者依赖的上下文等内容发生变更依旧会 re-render。注意,不是所有组件都要进行记忆化提升性能,记忆化也是具有成本的,需要衡量收益,一般 memo 函数用于组件会经常无效 re-render、组件本身渲染非常耗时等场景。
typescript
import { memo, useState } from "react";
const SlowComponent = memo(function SlowComponent() {
const words = Array.from({ length: 100_000 }, () => "WORD");
return (
<ul>
{words.map((word, i) => (<li key={i}>{i}: {word}</li>))}
</ul>
);
});
export default function App() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Slow counter?!? - {count}</h1>
<button onClick={() => setCount((c) => c + 1)}>Increase: {count}</button>
<SlowComponent />
</div>
);
}
依旧是优化 1 中的示例,我们将 SlowComponent 组件通过 memo 函数记忆化后,再次点击计数按钮,SlowComponent 组件没有发生 re-render,这是由于 SlowComponent 没有 props,也就是说不会有变化,因此父组件 re-render 就影响不到子组件了。
typescript
import { memo, useState } from "react";
const SlowComponent = memo(function SlowComponent({ config }) {
const words = Array.from({ length: 100_000 }, () => "WORD");
return (
<>
<p>{config.title}</p>
<ul>
{words.map((word, i) => (<li key={i}>{i}: {word}</li>))}
</ul>
</>
);
});
export default function App() {
const [count, setCount] = useState(0);
const config = {
title: "hello, world",
};
return (
<div>
<h1>Slow counter?!? - {count}</h1>
<button onClick={() => setCount((c) => c + 1)}>Increase: {count}</button>
<SlowComponent config={config} />
</div>
);
}
在 React 中,每一次 render 所有的内容都会被创建,包括 objects 和 functions,这会导致如果 objects 或 functions 作为 props,那么每一次父组件 re-render,子组件依旧会被 re-render,即使使用了 memo 函数将子组件记忆化,这是由于在 JS 中两个 objects 或 functions 看起来是一样,但实际是不等的,即{} !=={},例如上面的例子,config 是一个 object,即使不会发生改变,已经会认为将其作为 props 传入是不一样的,导致 SlowComponent 在使用 memo 函数后依旧被 re-render 了。
为了使 objects 和 functions 也能够进行记忆化,react 分别提供了 useMemo
和 useCallback
,其跟 useEffect 一样有一个依赖数组,只有依赖数组内容发生改变,值才会被重新创建,因此只要依赖内容保持不变,通过 useMemo 和 useCallback 的值会返回一样的内容。跟 memo 函数一样,并不是任何时刻都要通过 useMemo 和 useCallback 将内容进行记忆化,一般常用于以下几个场景:
- 将 props 记忆化避免过多的无效 re-render;
- 将值记忆化防止每次渲染都进行相同开销计算,有些类似于 Vue 中的 computed 功能;
- 将用于其他 hook 依赖数组中的内容进行记忆化,例如避免无穷的useEffect循环;
typescript
import { memo, useCallback, useMemo, useState } from "react";
const SlowComponent = memo(function SlowComponent({ config, onClick }) {
const words = Array.from({ length: 100_0 }, () => "WORD");
return (
<>
<p>{config.title}</p>
<ul>
{words.map((word, i) => (
<li key={i}>
{i}: {word} <button onClick={onClick}> click</button>
</li>
))}
</ul>
</>
);
});
export default function App() {
const [count, setCount] = useState(0);
const config = useMemo(() => {
// 一般可以进行一些复杂计算
return {
title: "hello, world",
};
}, []);
const handleClick = useCallback(() => {
setCount((c) => c + 1);
}, []);
return (
<div>
<h1>Slow counter?!? - {count}</h1>
<button onClick={handleClick}>Increase: {count}</button>
<SlowComponent config={config} onClick={handleClick} />
</div>
);
}
如上面例子,我们将传入 SlowComponent 的 props 中的 config(object)使用 useMemo 记忆化,将 onClick(function)使用 useCallback 记忆化,再次执行发现 SlowComponent 组件就没有发生 re-render 了。
应用案例
typescript
import { createContext, useMemo, useState } from "react";
const Context = createContext();
export function useContext() {
const context = useContext(Context);
if (context === undefined) throw new Error("Context was used outside the ContextProvider");
return context;
}
export default function MyContext({ children }) {
const [value, setValue] = useState(0);
const handleSetValue = (newValue) => {
setValue(newValue);
};
const contextValue = useMemo(() => {
return {
value,
handleSetValue,
};
}, [value]);
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
}
"通过 Children 或 Props 传递元素 "和"使用 Memoization"可以用来优化我们创建上下文 Context,首先我们通过 children 将我们的组件传入 Context.Provider 组件内,这样 Context.Provider 内容发生改变我们的组件不会一直被 re-render;如果如 Context.Provider 组件还有父组件,当父组件进行 re-render 时 Context.Provider 也会被 re-render,这会导致传递的 Value 都会被重新创建,所有使用这些 value 的组件都会被 re-render,这个时候我们可以使用 useMemo 函数对 value 进行缓存记忆化,注意,handleSetValue 函数会在 Context.Provider 组件 re-render 的时候重新创建,因此不需要将其写到依赖数组中,如果需要将其写到依赖数组中需要将其记忆化,这没有必要。
总结
本文介绍了基于 React 原生能力来进行性能优化减少无效 re-render 的两个方法,"通过 Children 或 Props 传递元素"和"使用 Memoization - memo/useMemo/useCallback"。通过 profiler developer tool 工具协助我们分析应用的组件渲染情况,从中找出无效渲染的场景,可以利用所介绍的方法来减少无效的 re-render,从而提高应用的速度和响应能力。这些原生的方法能够帮助我们提高系统性能,但是是不是我们一定都要使用这样的写法呢?答案当然是否定的,性能不要过早的进行优化,需要衡量性能优化的收益,优化本身也是有开销的,如果组件本身已经非常快速和精简了,就没有必要进这样的性能优化了。