介绍
我在 2021 年作为一名应届毕业生加入了一家网络开发公司,担任前端工程师,并将在 2022 年进入我的第二年。
在实践中,我主要使用 React x TypeScript 进行前端开发。
这一次,我将总结我在该领域所经历的 React 应用程序的性能优化。
本文的目标读者
- React 初学者到中级
- 想了解 React 性能优化的人
本文的目标
- 了解 React 渲染的工作原理
- 了解如何优化 React 性能
-
了解
React.memo、useCallback、useMemo
承诺
-
使用
React.memo、useCallback、useMemo的费用详解 - 不要用数字来衡量性能
关于以上两点,我会在相关地方附上一篇参考文章。
关于 React 渲染
为了优化 React 的性能,我将解释 React 的渲染机制。
首先,什么是渲染?React 文档测试版解释如下。 (使用 DeepL 翻译)
“渲染”是 React 调用你的组件的时候。
- 对于第一次渲染,React 调用根组件。
- 在后续渲染中,React 将调用触发渲染的状态更新的函数组件。
简而言之,让 React 调用你的函数组件渲染我正在调用它。
渲染的工作原理React 文档测试版描述如下。
- 触发渲染(将客户订单带到厨房)
- 组件渲染(在厨房准备订单)
- 提交到 DOM(将订单放入表中)
我将上面的渲染机制稍微分解并解释一下。
1.触发渲染
有两个触发渲染的事件:
- 第一次渲染
- 在屏幕刷新时重新渲染
重新渲染通过使用
state的更新函数setState更新状态并更新组件的状态来安排下一次渲染。 (获取差异并更新)2. React 渲染组件
渲染开始时,它会调用 React 组件来确定要在屏幕上显示什么。此时,不进行对DOM的反射处理。
- 第一次渲染:React 调用根组件
- 后续渲染:调用函数组件进行状态更新由 1 触发
如果更新的组件有子组件,React 接下来会渲染这些子组件。这个过程是递归的。
这种情况下,当父组件(Parent)更新状态时,子组件A、子组件B、子组件C、子组件D也会渲染。
代码如下所示:
父.tsx// 状態更新が発火する親コンポーネント export const Parent = () => { const [count, setCount] = useState<number>(0); const onClick = () => { setCount(count + 1); }; return ( <> <button onClick={onClick}>+1</button> <ChildA /> <ChildB /> </> ); };ChildA.tsxexport const ChildA = () => { return ( <> <p>子コンポーネントA</p> <ChildC /> </> ); };ChildB.tsxexport const ChildB = () => { return ( <> <p>子コンポーネントA</p> <ChildD /> </> ); };后面我会详细解释,但是当Parent组件的
state更新时,虽然只有Parent使用(ChildA到ChildD不依赖),但是ChildA到ChildD也会被渲染掉。您可以通过消除这种不必要的渲染来优化 React 的性能。
3. React 提交对 DOM 的更改
组件渲染后,React 将反映在 DOM 中,更新后的浏览器将重绘屏幕。
综上所述,React 应用程序的屏幕更新是通过以下 3 个步骤完成的
- 触发渲染
- 渲染组件
- 提交对 DOM 的更改
关于避免状态更新
如果您使用与当前值相同的值进行更新,React 将退出而不渲染任何子级或执行副作用。
如果
setState与当前state具有相同的值,它将避免渲染。让我们看一个具体的代码示例。
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。
即使之后点击按钮,当前
state为1,setState的参数值也为1,所以当前位置和更新值相同,所以不会进行渲染。 (避免)反应性能优化
在本文中,我们将介绍以下三种优化 React 性能的方法。
- React.memo
- 使用回调
- 使用备忘录
让我们仔细看看每一个。
反应备忘录
React.memo 是官方文件解释如下。
如果一个组件在给定相同的 props 时呈现相同的结果,您可以将其包装在 React.memo 中以记住结果并提高性能。换句话说,React 将跳过渲染组件并重用最后的渲染结果。
稍微分解一下,通过将
React.memo包裹在一个子组件中,如果子组件从父组件接收到的props的值没有改变,则子组件可以跳过渲染。这意味着您可以在不触发不必要的渲染的情况下提高性能。
[React.memo 的语法]
React.memo(メモ化したいコンポーネント);我们来看一个组件的例子,它管理
count并更新父子中不同count的状态。
[不使用 React.memo 时]父.tsxexport 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} /> </> ); };孩子.tsxtype ChildProps = { count: number; }; export const Child: React.FC<ChildProps> = ({ count }) => { return ( <> <p>子のcount:{count}</p> </> ); };如果单击按钮增加子组件的数量,您可以看到父子组件都呈现如下所示。
同样,点击按钮增加父组件的计数。您还可以确认父母和孩子都被渲染。
这里,当父组件的
parentCount更新时,即使依赖于子组件的值没有更新,渲染进程也在运行。用
React.memo包裹子组件以避免这种不必要的渲染。孩子.tsxexport const Child: React.FC<ChildProps> = ({ count }) => { console.log("子供コンポーネントのレンダリング"); return ( <> <p>子のカウント:{count}</p> </> ); }; export const ChildMemo = React.memo(Child);如果再次点击增加父组件计数的按钮,可以看到只渲染父组件,没有更新值的
ChildMemo组件没有渲染。
通过如上所述使用
React.memo,您可以通过在props传递的值没有变化时避免不必要的渲染来优化性能。使用回调
useCallback是官方文件解释如下。传递一个内联回调数组和它们所依赖的值。 useCallback 返回该回调的记忆版本,并且该函数仅在任何依赖数组元素发生更改时才更改。
这对于将回调传递给经过优化以查找引用相等性的组件(例如,使用 shouldComponentUpdate )以避免不必要的渲染非常有用。稍微分解一下,只有当
useCallback的第二个参数指定的依赖数组的任何元素发生变化时,它才会重新计算记忆值。换句话说,如果依赖数组的元素没有改变,就可以避免不必要的渲染。
[useCallback 的语法]
useCallback(コールバック関数, 依存配列);让我们仔细看看代码。
除了子组件中的
count,还可以从props的父组件接收更新(+1)count状态的函数(onClickChild)。孩子.tsxtype 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);父.tsxexport 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。
然后,您可以看到子组件已被渲染,即使该组件之前已被记忆。
原因是在
props接收到的onClickChild在每次父组件重新渲染时都会重新计算,结果在onClickChild被传递给React的时机,一个新的函数被传递给propsside 知道并且即使在子组件中也正在运行渲染。因此,即使使用
React.memo记忆组件主体,渲染也会运行。为了避免这种不必要的渲染,
props中传递的函数 (onClickChild) 被包裹在useCallback中并被记忆。父.tsxexport 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); }, []);为了防止这种情况发生,通过插入依赖于
useCallback的第二个参数的依赖数组的状态(这次是childCount),在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 只会重新计算记忆值。这种优化避免了在每次渲染时执行昂贵的计算。
React.memomemoized 组件和useCallbackmemoized 回调函数,但是useMemomemoize 计算值(数字和渲染结果),避免不必要的渲染。[useMemo 的语法]
useMemo(() => メモ化したい計算ロジック, 依存配列);具体来说,之前使用
React.memo转换为memo的组件中的JSX 将使用useMemo进行记忆。[之前用 React.memo 记忆的子组件]
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]); };我将
console放在useMemo的内部和外部,以便于确认记忆。我会尝试增加父组件的数量。
然后你可以看到没有被记忆的
console.log("子供コンポーネントのレンダリング");(在useMemo之外)正在运行。在前面介绍的 React.memo 中,整个组件都是 memoized 的,所以在执行 parent count 的时候子组件没有渲染。
接下来,点击按钮增加子组件的
count。
然后可以看到
useMemo处记忆的JSX值也被执行了。在最后
怎么样。在本文中,我们总结了优化 React 性能的基本方法。
我们希望您使用这里介绍的方法来开发性能更好的产品。
下一次,我想介绍如下数据通信中的性能优化。
- 使用 useQuery 和 useSWR 优化获取
我还有其他关于 React 的文章,所以如果你能阅读它们,我会很高兴。
原创声明:本文系作者授权爱码网发表,未经许可,不得转载;
原文地址:https://www.likecs.com/show-308628151.html