React性能优化概述
首先
我在2021年以新入职的形式加入了一家Web开发公司,担任前端工程师的职位,现在是2022年,已经是我的第二个年头了。
我主要在实际工作中使用React×TypeScript进行前端开发。
这次将总结我在现场经历的React应用程序性能优化。
本文的受众群
-
- Reactの初心者から中級者
- Reactのパフォーマンス最適化について学びたい人
这篇文章的目标
-
- Reactのレンダリングの仕組みを理解する
-
- Reactのパフォーマンス最適化の方法を知る
React.memo, useCallback, useMemoについて理解する
请允许我解释
React.memo, useCallback, useMemoを使うコストについての詳しい解説
パフォーマンスの数値的な計測は行いません
关于上述两点,我将在相关位置附上参考文章链接。
关于React的渲染机制
在进行React性能优化时,我们将解释React渲染机制。
首先,渲染在React Docs BETA中被解释如下(已使用DeepL翻译)
「渲染」是指React调用组件的过程。
在首次渲染中,React会调用根组件。
在后续的渲染中,React会调用触发渲染的状态更新函数组件。
简单来说,React将调用函数组件并渲染其内容称为”渲染”。
在React文档BETA中,渲染机制被描述如下。
触发渲染(将客户的订单送达到厨房)
组件渲染(在厨房准备订单)
提交至DOM(将订单放在餐桌上)
我将简明解释上述渲染机制。
触发渲染
触发渲染的事件有以下两个选项:
-
- 初回レンダリング
- 画面更新時の再レンダリング
通过使用状态更新函数setState来更新状态,并通过更新组件的状态来安排下次渲染,从而实现重新渲染(获取并更新差分)。
React会对组件进行渲染。
当开始渲染时,会调用React组件来确定要在屏幕上显示的内容。在这个时点上,还没有进行DOM的反映处理。
-
- 初回レンダリング: Reactはルートコンポーネントを呼び出す
- それ以降のレンダリング: 1でトリガーとなった状態更新の関数コンポーネントを呼び出す
当组件更新且拥有子组件时,React会接着渲染这些子组件。这个过程是递归进行的。

如果在这种情况下,父组件更新了状态,子组件A、子组件B、子组件C、子组件D也会触发渲染。
在代码中如下所示。
// 状態更新が発火する親コンポーネント
export const Parent = () => {
const [count, setCount] = useState<number>(0);
const onClick = () => {
setCount(count + 1);
};
return (
<>
<button onClick={onClick}>+1</button>
<ChildA />
<ChildB />
</>
);
};
export const ChildA = () => {
return (
<>
<p>子コンポーネントA</p>
<ChildC />
</>
);
};
export const ChildB = () => {
return (
<>
<p>子コンポーネントA</p>
<ChildD />
</>
);
};
稍後會詳細解釋,當 Parent 組件的狀態更新時,即使 ChildA 到 ChildD 無需依賴它們,仍然會導致 ChildA 到 ChildD 的渲染處理被執行。
通过消除这种不必要的渲染,可以优化React的性能。
3. React 将更改提交到 DOM。
在组件进行渲染后,React会将更新后的内容反映到DOM上,从而使浏览器重新绘制屏幕。
概括来说,React应用程序的界面更新可以通过以下三个步骤完成。
-
- レンダリングをトリガー
-
- コンポーネントをレンダリング
- DOMへ変更をコミット
关于避免State更新
从React官方文档中,
如果使用相同的值进行更新,React 会避免进行子级渲染和副作用执行,并结束处理。
如果将当前状态和相同的值作为setState的参数传入,将避免重新渲染。
我来看看有关代码的具体例子。
export const Parent = () => {
const [count, setCount] = useState<number>(0);
const onClick = () => {
setCount(1);
};
console.log("レンダリング");
return (
<>
<button onClick={onClick}>+1</button>
<p>count: {count}</p>
</>
);
};
在上述的组件中,首次呈现和按钮点击时,计数会更新为1,从而触发重新呈现。
点击按钮后,如果当前的状态为1,并且setState的参数值也为1,则当前位置和更新值相同,因此不会发生重新渲染。(被避免)
React性能优化
在进行React性能优化时,我们这次将介绍以下三个方面。
-
- React.memo
-
- useCallback
- useMemo
我们将分别详细地进行查看。
记忆化组件 (React.memo)
React.memo在官方文档中如下解释所述。
如果一个组件在给定相同的 props 后渲染出相同的结果,可以用 React.memo 包裹它来记忆结果以提高性能。换句话说,React 可以跳过组件的渲染并重用上一次的渲染结果。
当使用React.memo将子组件包裹起来后,稍作修改即可使得子组件在接收来自父组件的props时,如果值没有发生变化,则可以跳过子组件的重新渲染。
换句话说,您可以提高性能而不触发不必要的渲染。
【React.memo的语法】
React.memo(メモ化したいコンポーネント);
让我们以一个组件的例子来确认父母和子女管理和更新不同的计数和状态。

export const Parent = () => {
const [parentCount, setParentCount] = useState<number>(0);
const [childCount, setChildCount] = useState<number>(0);
const addParentCount = () => {
setParentCount(parentCount + 1);
};
const addChildCount = () => {
setChildCount(childCount + 1);
};
return (
<>
<button onClick={addParentCount}>親のカウントを+1</button>
<p>親のカウント: {parentCount}</p>
<button onClick={addChildCount}>子のカウントを+1</button>
<Child count={childCount} />
</>
);
};
type ChildProps = {
count: number;
};
export const Child: React.FC<ChildProps> = ({ count }) => {
return (
<>
<p>子のcount:{count}</p>
</>
);
};
点击增加子组件计数的按钮后,可以确认父组件和子组件都已被渲染如下所示。

我們點擊同樣增加父組件計數的按鈕,在這樣做時,我們可以確認父組件和子組件都會重新渲染。

当父组件的parentCount在此处被更新时,尽管依赖于它的子组件的值未被更新,但渲染仍将进行处理。
为了避免进行不必要的渲染,我们可以使用React.memo将子组件进行封装。
export const Child: React.FC<ChildProps> = ({ count }) => {
console.log("子供コンポーネントのレンダリング");
return (
<>
<p>子のカウント:{count}</p>
</>
);
};
export const ChildMemo = React.memo(Child);
点击增加再次、亲组件的计数按钮时,只有亲组件会被渲染,没有值更新的ChildMemo组件不会被渲染,可以确认这一点。

通过使用React.memo,可以避免在props传递的值没有变化时进行不必要的渲染,从而优化性能。
使用useCallback
在官方文件中,useCallback被解释如下:
请传递内联回调和它所依赖的值数组。useCallback返回一个记忆化的回调,并且该函数只有在依赖数组的元素发生变化时才会发生变化。
这在将回调传递给优化了参考的组件时非常方便,这样可以避免不必要的渲染(例如使用shouldComponentUpdate等),以便提高性能。
若稍微简化一下,只有当useCallback中指定的依赖数组的任何一个元素发生变化时,才会重新计算记忆化的值。
如果依赖数组的元素没有发生变化,就可以避免进行不必要的渲染。
【useCallback的语法】
【useCallback的语法】的用法是需要先传入一个回调函数和一个依赖项数组。
useCallback(コールバック関数, 依存配列);
我们将通过一个代码示例来具体解释。
在子组件中,除了接收count属性外,还能够通过props从父组件接收一个能够更新count状态(+1)的函数(onClickChild)。
type ChildProps = {
count: number;
onClickChild: () => void;
};
export const Child: React.FC<ChildProps> = ({ count, onClickChild }) => {
console.log("子供コンポーネントのレンダリング");
return (
<>
<button onClick={onClickChild}>子のカウントを+1</button>
<p>子のカウント:{count}</p>
</>
);
};
export const ChildMemo = React.memo(Child);
export const Parent = () => {
const [parentCount, setParentCount] = useState<number>(0);
const [childCount, setChildCount] = useState<number>(0);
const addParentCount = () => {
setParentCount(parentCount + 1);
};
const addChildCount = () => {
setChildCount(childCount + 1);
};
console.log("親コンポーネントのレンダリング");
return (
<>
<button onClick={addParentCount}>親のカウントを+1</button>
<p>親のカウント: {parentCount}</p>
<ChildMemo count={childCount} onClickChild={addChildCount} />
</>
);
};

当你点击增加子计数按钮(onClickChild)时,在控制台中可以确认父组件和子组件都被重新渲染了。

然后点击按钮来更新不依赖于子组件的父组件的count。

然后,尽管刚刚对组件进行了Memo化,但是可以确认子组件仍然被渲染出来。
原因之一是, onClickChild 在 props 中接收,每当父组件重新渲染时,都会重新计算 onClickChild ,结果是在 props 中传递的 onClickChild 函数会在 React 在子组件中也意识到,并触发重新渲染。
因此,即使使用React.memo对组件本体进行了记忆化,渲染仍会进行。
为了避免不必要的渲染,请使用`useCallback`将传递给`props`的函数(`onClickChild`)进行封装和记忆化。
export const Parent = () => {
const [parentCount, setParentCount] = useState<number>(0);
const [childCount, setChildCount] = useState<number>(0);
const addParentCount = () => {
setParentCount(parentCount + 1);
};
// メモ化
const addChildCount = useCallback(() => {
setChildCount(childCount + 1);
}, []);
console.log("親コンポーネントのレンダリング");
return (
<>
<button onClick={addParentCount}>親のカウントを+1</button>
<p>親のカウント: {parentCount}</p>
<ChildMemo count={childCount} onClickChild={addChildCount} />
</>
);
};
只要按下增加父级计数的按钮,就可以确认子级的渲染已被避免。

然而,当点击增加子组件计数的按钮时,出现了无法从1增加的问题。

由于在之前定义的useCallback函数的第二个参数中传递了一个空数组,所以导致在首次渲染时,通过useCallback包装的函数会一直被React内部保持。
在代码中,setChildCount的值被持续保持为1,导致从1开始的值无法更新。
const [childCount, setChildCount] = useState<number>(0);
const addChildCount = useCallback(() => {
// setChildCount(childCount + 1);
// setChildCount(0 + 1);
setChildCount(1);
}, []);
为了防止这种情况发生,可以将与state(本例中是childCount)有依赖关系的依赖数组作为useCallback的第二个参数,这样在childCount的值更新时,useCallback内定义的函数会被重新计算。
const [childCount, setChildCount] = useState<number>(0);
const addChildCount = useCallback(() => {
setChildCount(childCount + 1);
// 1回目ボタンがクリックされた時
// setChildCount(1);
// 2回目ボタンがクリックされた時
// setChildCount(1+1);
// 3回目ボタンがクリックされた時
// setChildCount(2+1);
}, [childCount]);
通过使用useCallback,可以避免在将函数通过props传递时产生不必要的渲染。
使用 useMemo
useMemo在官方文档中是这样解释的。
如果依赖数组的任何元素发生变化,useMemo将重新计算缓存的值。通过这种优化,可以避免在每次渲染时执行昂贵的计算。
React.memo用于对组件进行记忆化处理,useCallback用于对回调函数进行记忆化处理,而useMemo则可以对计算结果的值(如数值或渲染结果)进行记忆化处理,以避免不必要的重新渲染。
【useMemo语法】:
useMemo(() => メモ化したい計算ロジック, 依存配列);
我们将使用useMemo来记忆化先前使用React.memo进行记忆化的组件内的JSX。
先前使用 React.memo 进行了记忆的 Child 组件
type ChildProps = {
count: number;
onClickChild: () => void;
};
export const Child: React.FC<ChildProps> = ({ count, onClickChild }) => {
console.log("子供コンポーネントのレンダリング");
return (
<>
<button onClick={onClickChild}>子のカウントを+1</button>
<p>子のカウント:{count}</p>
</>
);
};
export const ChildMemo = React.memo(Child);
使用useMemo来进行JSX的记忆化
type ChildProps = {
count: number;
onClickChild: () => void;
};
export const Child: React.FC<ChildProps> = ({ count, onClickChild }) => {
console.log("子供コンポーネントのレンダリング");
return useMemo(() => {
console.log("メモ化した値");
return (
<>
<button onClick={onClickChild}>子のカウントを+1</button>
<p>子のカウント:{count}</p>
</>
);
}, [count, onClickChild]);
};
为了更直观地确认记忆化,我在useMemo的内部和外部都加入了console。
我将增加父组件的计数并验证其行为。

然后,您可以确认在没有执行useMemo的情况下(位于useMemo之外),console.log(“子组件渲染”)被执行了。
在之前介绍的React.memo中,由于对整个组件进行了记忆化处理,所以当执行父组件的计数时,子组件不会重新渲染。

接下来,我们尝试点击子组件的按钮来增加count的数量。

那么,我们可以确认通过useMemo进行了记忆化的JSX的值也被执行了。
最后
这次总结了一些优化React性能的基本方法,您觉得如何?
我希望您能够使用我介绍的方法来进行更高性能的开发。
我打算在下一次中,介绍有关数据通信性能优化的一些内容。
- useQueryやuseSWRを利用したフェッチ処理の最適化
希望您能阅读其他我写的有关React的文章,我会感到非常高兴。