怎样对react,hooks进行性能优化?

本文介绍如何使用React.memo、useMemo及useCallback优化React应用性能,避免不必要的组件重渲染和重复计算。

前言

现在越来越多人开始使用 React Hooks + 函数组件的方式构筑页面。函数组件简洁且优雅,通过 Hooks 可以让函数组件拥有内部的状态和副作用(生命周期),弥补了函数组件的不足。

但同时函数组件的使用也带来了一些额外的问题:由于函数式组件内部的状态更新时,会重新执行一遍函数,那么就有可能造成以下两点性能问题:

  1. 造成子组件的非必要重新渲染
  2. 造成组件内部某些代码(计算)的重复执行

好在 React 团队也意识到函数组件可能发生的性能问题,并提供了 React.memouseMemouseCallback 这些 API 帮助开发者去优化他们的 React 代码。在使用它们进行优化之前,我想我们需要明确我们使用它们的目的:

  1. 减少组件的非必要重新渲染
  2. 减少组件内部的重复计算

1 使用 React.memo 避免组件的重复渲染

在讲述 React.memo 的作用之前,我们先来思考一个问题:什么情况下需要重新渲染组件?

一般来讲以下三种情况需要重新渲染组件:

  1. 组件内部 state 发生变化时
  2. 组件内部使用的 context 发生变化时
  3. 组件外部传递的 props 发生变化时

现在我们先只关注第 3 点:props 发生变化时重新渲染,这种情况是一种理想情况。因为如果一个父组件重新渲染,即使其子组件的 props 没有发生任何变化,这个子组件也会重新渲染,我们称这种渲染为非必要的重新渲染。这时 React.memo 就可以派上用场了。

首先 React.memo 是一个高阶组件

高阶组件(Higher Order Component)类似一个工厂:将一个组件丢进去,然后返回一个被加工过的组件。

React.memo 包裹的组件在渲染前,会对新旧 props 进行浅比较

  • 如果新旧 props 浅比较相等,则不进行重新渲染(使用缓存的组件)。
  • 如果新旧 props 浅比较不相等,则进行重新渲染(重新渲染的组件)。

上述的解释可能会比较抽象,我们来看一个具体的例子:

import React, { useState } from 'react';

const Child = () => {
  console.log('Child 渲染了');
  return <div>Child</div>;
};

const MemoChild = React.memo(() => {
  console.log('MemoChild 渲染了');
  return <div>MemoChild</div>;
});

function App() {
  const [isUpdate, setIsUpdate] = useState(true);
  const onClick = () => {
    setIsUpdate(!isUpdate);
    console.log('点击了按钮');
  };
  return (
    <div className="App">
      <Child />
      <MemoChild />
      <button onClick={onClick}>刷新 App </button>
    </div>
  );
}

export default App;
复制代码

上例中:Child 是一个普通的组件,MemoChild 是一个被 React.memo 包裹的组件。

当我点击 button 按钮时,调用 setIsUpdate 触发 App 组件重新渲染(re-render)。

控制台结果如下:

qpeoctpng

如上图:
首次渲染时,ChildMemoChild 都会被渲染,控制台打印 Child 渲染了memoChild 渲染了。

而当我点击按钮触发重新渲染后,Child 依旧会重新渲染,而 MemoChild 则会进行新旧 props 的判断,由于 memoChild 没有 props,即新旧 props 相等(都为空),则 memoChild 使用之前的渲染结果(缓存),避免了重新渲染。

由此可见,在没有任何优化的情况下,React 中某一组件重新渲染,会导致其全部的子组件重新渲染。即通过 React.memo 的包裹,在其父组件重新渲染时,可以避免这个组件的非必要重新渲染。

需要注意的是:上文中的【渲染】指的是 React 执行函数组件并生成或更新虚拟 DOM 树(Fiber 树)的过程。在渲染真实 DOM (Commit 阶段)前还有 DOM Diff 的过程,会比对虚拟 DOM 之间的差异,再去渲染变化的 DOM 。不然如果每次更改状态都会重新渲染真实 DOM,那么 React 的性能真就爆炸了(笑)。

更多react面试题解答参见 前端react面试题详细解答

2 使用 useMemo 避免重复计算

const memolized = useMemo(fn,deps)

React 的 useMemo 把【计算函数 fn】和【依赖项数组 deps】作为参数,useMemo 会执行 fn 并返回一个【缓存值 memolized】,它仅会在某个依赖项改变时才重新计算 memolized。这种优化有助于避免在每次渲染时都进行高开销的计算。具体使用场景可以参考下例:

import React, { useMemo, useState } from 'react';

function App() {
  const [list] = useState([1, 2, 3, 4]);
  const [isUpdate, setIsUpdate] = useState(true);
  const onClick = () => {
    setIsUpdate(!isUpdate);
    console.log('点击了按钮');
  };

  // 普通计算 list 的和
  console.log('普通计算');
  const sum = list.reduce((previous, current) => previous + current);

  // 缓存计算 list 的和
  const memoSum = useMemo(() => {
    console.log('useMemo 计算');
    return list.reduce((previous, current) => previous + current);
  }, [list]);

  return (
    <div className="App">
      <div> sum:{sum}</div>
      <div> memoSum:{memoSum}</div>
      <button onClick={onClick}>重新渲染 App</button>
    </div>
  );
}

export default App;
复制代码

上例中:sum 是一个根据 list 得到的普通计算值,memoSum 是一个通过 useMemo 得到的 momelized 值(缓存值),并且依赖项为 list

qpR3CTpng

如上图控制台中 log 所示:

  1. 首次渲染,summemoSum 都会根据 list 的值进行计算;

  2. 当点击 【重新渲染 App】按钮后,虽然 list 没有改变,但是 sum 的值进行了重新计算,而 memoSum 的值则没有重新计算,使用了上一次的计算结果(memolized)。

  3. 当点击 【往 List 添加一个数字】按钮后,list 的值发生改变,summemoSum 的值都进行重新计算。

总结:在函数组件内部,一些基于 State 的衍生值和一些复杂的计算可以通过 useMemo 进行性能优化。

3 使用 useCallback 避免子组件的重复渲染

const memolizedCallback = useCallback(fn, deps);

React 的 useCallback 把【回调函数 fn】和【依赖项数组 deps】作为参数,并返回一个【缓存的回调函数 memolizedCallback】(本质上是一个引用),它仅会在某个依赖项改变时才重新生成 memolizedCallback。当你把 memolizedCallback 作为参数传递给子组件(被 React.memo 包裹过的)时,它可以避免非必要的子组件重新渲染。

useCallback 与 useMemo 异同

useCallbackuseMemo 都会缓存对应的值,并且只有在依赖变动的时候才会更新缓存,区别在于:

  • useMemo 会执行传入的回调函数,返回的是函数执行的结果
  • useCallback 不会执行传入的回调函数,返回的是函数的引用

useCallback 使用误区

有很多初学者(包括以前的我)会有这样一个误区:在函数组件内部声明的函数全部都用 useCallback 包裹一层,以为这样可以通过避免函数的重复生成优化性能,实则不然:

  1. 首先,在 JS 内部函数创建是非常快的,这点性能问题不是个问题(参考:React 官方文档:Hook 会因为在渲染时创建函数而变慢吗?
  2. 其次,使用 useCallback 会造成额外的性能损耗,因为增加了额外的 deps 变化判断。
  3. 每个函数用 useCallback 包一层,不仅显得臃肿,而且还需要手写 deps 数组,额外增加心智负担。

useCallback 正确的使用场景

  1. 函数组件内部定义的函数需要作为其他 Hooks 的依赖
  2. 函数组件内部定义的函数需要传递给其子组件,并且子组件由 React.memo 包裹

场景 1:useCallback 主要是为了避免当组件重新渲染时,函数引用变动所导致其它 Hooks 的重新执行,更为甚者可能造成组件的无限渲染:

import React, { useEffect, useState } from 'react';

function App() {
  const [count, setCount] = useState(1);
  const add = () => {
    setCount((count) => count + 1);
  };
  useEffect(() => {
    add();
  }, [add]);
  return <div className="App">count: {count}</div>;
}

export default App;
复制代码

上例中,useEffect 会执行 add 函数从而触发组件的重新渲染,函数的重新渲染会重新生成 add 的引用,从而触发 useEffect 的重新执行,然后再执行 add 函数触发组件的重新渲染… ,从而导致无限循环:

useEffect 执行 -> add 执行 -> setCount 执行 -> App 重新渲染 -> add 重新生成 -> useEffect 执行 -> add 执行 -> …

为了避免上述的情况,我们给 add 函数套一层 useCallback 避免函数引用的变动,就可以解决无限循环的问题:

import React, { useCallback, useEffect, useState } from 'react';

function App() {
  const [count, setCount] = useState(1);
  // 用 useCallback 包裹 add ,只会在组件第一次渲染生成函数引用,之后组件重新渲染时,add 会复用第一次生成的引用。
  const add = useCallback(() => {
    setCount((count) => count + 1);
  }, []);
  useEffect(() => {
    add();
  }, [add]);
  return <div className="App">count: {count}</div>;
}

export default App;
复制代码

场景 2:useCallback 是为了避免由于回调函数引用变动,所导致的子组件非必要重新渲染。(这个子组件有两个前提:首先是接收回调函数作为 props,其次是被 React.memo 所包裹。)

const Child = React.memo(({ onClick }) => {
  console.log(`Button render`);
  return (
    <div>
      <button onClick={onClick}>child button</button>
    </div>
  );
});

function App() {
  const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);
  // 情况1:未包裹 useCallback
  const onClick = () => {
    setCountA(countA + 1);
  };
  // 情况2:包裹 useCallback
  const onClick = useCallback(() => {
    setCountA(countA + 1);
  }, []);
  return (
    <div>
      <div>countA:{countA}</div>
      <div>countB:{countB}</div>
      <Child onClick={onClick1} />
      <button onClick={() => setCountB(countB + 1)}>App button</button>
    </div>
  );
}
复制代码

上例中,Child 子组件由 React.memo 包裹,接收 onClick 函数作为 props 参数。

  • 情况 1:onClick 未包裹 useCallback ,当点击 app button 时,触发重新渲染,onClick 重新生成函数引用,导致 Child 子组件重新渲染。
  • 情况 2:onClick 包裹 useCallback ,当点击 app button 时,触发重新渲染,onClick 不会生成新的引用,避免了 Child 子组件重新渲染。

4 总结

上文叙述中,我们通过 React.memouseMemouseCallback 这些 API 避免了在使用函数组件的过程中可能触发的性能问题,总结为一下三点:

  • 通过 React.memo 包裹组件,可以避免组件的非必要重新渲染。
  • 通过 useMemo,可以避免组件更新时所引发的重复计算。
  • 通过 useCallback,可以避免由于函数引用变动所导致的组件重复渲染。
<think>好的,我现在需要回答用户关于React Hooks性能优化中的常见手段的问题。首先,我得回忆一下React Hooks的相关知识,特别是性能优化方面。用户之前可能了解过类组件的优化方法,现在转向Hooks,所以需要突出Hooks特有的优化手段。首先,常见的优化手段包括使用useMemo和useCallback来避免不必要的计算和重新渲染。这两个Hook是关键,可以缓存值和函数,防止子组件无意义的更新。需要具体说明它们的用法和适用场景。 然后,React.memo也是一个重要点,它用于记忆组件,避免父组件更新时子组件跟着渲染。但要注意,如果props中有未正确缓存的函数或对象,React.memo可能失效,这时候需要结合useCallback或useMemo使用。 接下来,可能要考虑useEffect的依赖项优化。正确设置依赖数组可以减少副作用执行的次数,避免不必要的处理。比如空数组表示只在挂载和卸载时执行,或者精确列出依赖项的变化。 还有,状态管理的优化,比如使用useReducer代替多个useState,尤其是在状态逻辑复杂时,可以减少重复渲染。此外,ContextAPI的使用也可能导致性能问题,如何通过记忆化或分割Context来优化。 另外,惰性初始化也是值得提到的,比如useState的初始化函数可以避免每次渲染都执行高开销计算。这适用于初始状态需要复杂计算的场景。 用户提供的引用中提到了React.memo、代码分割、避免内联函数等,这些在类组件中也适用,但在Hooks中实现方式不同,需要结合Hooks的特性来讲解。例如,如何用useCallback来避免内联函数的重新创建。 还要注意引用中的例子,比如使用React.lazy和Suspense进行代码分割,虽然这更多是代码加载优化,但也可以作为性能优化的一部分提及,尤其是在Hooks组件中的应用。 可能还需要提到useRef来保持引用不变,避免不必要的重新渲染,尤其是在保存DOM节点或定时器ID时。最后,总结这些手段,强调在Hooks中如何组合使用这些方法,同时指出应根据具体场景选择合适的优化策略,避免过度优化。</think>### React Hooks 性能优化常见手段 React Hooks性能优化核心在于**减少不必要的渲染**和**高效管理副作用**。以下是具体手段: --- #### 1. **缓存值与函数:`useMemo` 与 `useCallback`** - **`useMemo`**:缓存计算结果,避免重复计算。 ```javascript const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); ``` 适用于复杂计算或依赖特定变量的场景,如过滤列表、数据转换[^1]。 - **`useCallback`**:缓存函数引用,避免子组件因函数变化而重新渲染。 ```javascript const memoizedFn = useCallback(() => doSomething(a, b), [a, b]); ``` 常用于将函数作为props传递给子组件时[^2]。 --- #### 2. **组件渲染优化:`React.memo`** - 使用 `React.memo` 包裹函数组件,仅在props变化时重新渲染: ```javascript const MemoizedComponent = React.memo(({ data }) => { return <div>{data}</div>; }); ``` 需配合 `useMemo` 或 `useCallback` 确保props中的对象或函数引用稳定[^3]。 --- #### 3. **避免重复副作用:优化 `useEffect` 依赖项** - 精确指定依赖数组,避免副作用频繁触发: ```javascript useEffect(() => { fetchData(id); }, [id]); // 仅当id变化时执行 ``` - 空依赖数组表示仅在挂载/卸载时执行: ```javascript useEffect(() => { initSocket(); return () => closeSocket(); }, []); ``` --- #### 4. **状态管理优化** - **惰性初始化**:通过函数延迟初始状态的计算,避免每次渲染都执行: ```javascript const [state] = useState(() => computeInitialState()); ``` - **合并状态**:使用 `useReducer` 替代多个 `useState`,减少渲染次数: ```javascript const [state, dispatch] = useReducer(reducer, initialState); ``` --- #### 5. **Context 优化** - 分割多个Context,避免单一Context变化导致所有消费者更新: ```javascript const UserContext = React.createContext(); const ThemeContext = React.createContext(); ``` - 在子组件中通过 `useContext` 选择性订阅Context。 --- #### 6. **代码分割与懒加载** - 使用 `React.lazy` 和 `Suspense` 动态加载组件,减少首屏资源体积: ```javascript const LazyComponent = React.lazy(() => import('./LazyComponent')); ``` 结合 `import()` 语法实现按需加载[^4]。 --- #### 7. **避免内联对象与函数** - 内联对象或函数会导致每次渲染生成新引用,破坏 `React.memo` 优化: ```javascript // 不推荐 <ChildComponent style={{ color: 'red' }} onClick={() => {}} /> // 推荐:使用useMemo/useCallback缓存 const style = useMemo(() => ({ color: 'red' }), []); const onClick = useCallback(() => {}, []); ``` --- ### 总结 优化核心是**控制渲染次数**与**减少计算开销**,需结合具体场景选择手段。例如: - 列表渲染优先用 `useMemo` 缓存数据。 - 高频交互场景用 `useCallback` 稳定函数引用。 - 复杂状态逻辑用 `useReducer` 替代分散的 `useState`。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值