React性能优化的实现方法详解

React
288
0
0
2023-07-27
目录
  • 前言
  • 遍历视图key使用
  • React.memo缓存组件
  • React.useCallback让函数保持相同的引用
  • 避免使用内联对象
  • 使用React.useMemo缓存计算结果或者组件
  • 使用React.Fragment片段
  • 组件懒加载
  • 通过 CSS 加载和卸载组件
  • 变与不变的地方做分离
  • 总结

前言

想要写出高质量的代码,仅仅靠框架底层帮我们的优化还远远不够,在编写的过程中,需要我们自己去使用提高的 api,或者根据它底层的原理去做一些优化,以及规范。

相比于 Vue ,React 不会再框架源码层面帮助我们直接解决一下基本的性能优化相关,而是提供一下 API (Hooks)让我们自己去优化我们的应用,也是它自身更灵活的一种原因之一。

下面总结了一些从编写 React 代码层面上能做的优化点。

遍历视图key使用

key 的作用能够帮助我们识别哪些元素改变了,比如添加和删除。在 React 更新时,会触发 React Diff 算法,diff 过程中过借助 key 值来判断元素是新创建还是需要移动的元素。React 会保存这个辅助状态。从而减少不必要的元素渲染。

key 的值最好是当前列表中拥有独一无二的字符串。开发中通常用 id 等这些作为元素的 key 值。

当前的列表不会发生操作,万不得已 可以使用 index 作为 key 值。

key 应该具有稳定,可预测,以及列表内唯一的特质。不稳定的 key 比如 Math.random() 生成的会 导致很多组件实例和 DOM 节点被不必要的重新创建,这可能导致性能下降和子组件状态丢失等等。

React.memo缓存组件

react 是单向数据流,父组件状态的更新也会让子组件一起重新渲染更新,即使子组件的状态没有发生变化,不会像 Vue 一样能够具体监听到某一个组件状态的变化然后更新当前的这个组件。

因此可以用 React.memo 缓存组件,这样只有传入当前组件状态值变化时才会重新渲染,值相同那么就会缓存组件。

// 子组件
const Child = React.memo(() => {
  console.log("child");
  return (
    <div>
      Child
    </div>
  );
});
// 父组件
function App() {
  const [count, setCount] = useState();
  return (
    <div className="App">
      <h>{count}</h3>
      <button onClick={() => setCount(count +)}>Count++ </button>
      <Child />
    </div>
  );
}

上面代码 <Child /> 组件添加上 memo 每次点击 count ++ 那么就会不会重新渲染了。

React.useCallback让函数保持相同的引用

像上面的例子,如果父组件想拿到子组件的状态值,通常会使用 callback 的方式传递出去给父组件。

interface ChildProps {
  onChangeNum: (value: number) => void;
}
const Child: React.FC<ChildProps> = React.memo(({ onChangeNum }) => {
  console.log("child");
  const [num, setNum] = useState();
  useEffect(() => {
    onChangeNum(num);
  }, [num]);
  return (
    <div>
      <button
        onClick={() => {
          setNum((prevState) => {
            return prevState +;
          });
        }}
      >
        Child
      </button>
    </div>
  );
});
function App() {
  const [count, setCount] = useState();
  return (
    <div className="App">
      <h>{count}</h3>
      <button onClick={() => setCount(count +)}>Count++ </button>
      <Child
        onChangeNum={(num) => {
          console.log(num, "childNum");
        }}
      />
    </div>
  );
}

组件每次更新 num 值,父组件通过 onChangeNum 回掉函数方式接受。

注意刚才说的 memo 能够在组件传入值不变的情况下缓存组件避免重新渲染,但是,这里又失效了。这是为什么呢?

原因就是父组件更新了,每次都会创建一个新的 onChangeNum ,相当于属于不同的引用了,在每次 props 传递的回掉函数都不相同,所以 memo 失去了作用。

那么该怎么解决?那就是使用 useCallback hook 帮助我们保持相同的引用。

<Child
  onChangeNum={useCallback((num) => {
    console.log(num, "childNum");
  }, [])}
/>

开发中使用了 memo 缓存了组件,还需要注意是否有匿名函数传递给子组件。

并不一定只在这种情况下才使用 useCallback ,比如一个请求函数或者逻辑处理函数,也可以用 useCallback 包裹,不过要注意,内部引用了外部的状态或者值的相关联,那么需要在第二个参数也就是依赖数组里面添加上用到的某些值。

避免使用内联对象

在使用内联对象,react 每次重新渲染时会重新创建此对象,在更新组件对比 props ,oldProps === newProps 只要为 false 那么就会 re-render 。

如果TestComponent 组件重新渲染,那么就会新建创建 someProps 引用。传递给 RootComponent 组件每次判断新旧 props 结果不同,导致也重新渲染。

const TestComponent = () => {
  const someProps = { value: '' }
  return <RootComponent someProps={someProps} />;
};

更好的方式是,使用 ES6 扩展运算符的将这个对象展开,引用类型变为值类型传递,这样再对比 props 就会相等了。

const TestComponent = () => {
  const someProps = { value: '' }
  return <RootComponent {...someProps} />;
};

使用React.useMemo缓存计算结果或者组件

如 React 文档所说,useMemo 的基本作用是,避免每次渲染都进行高开销的计算。

如果是一个功能组件里面,涉及到大型的计算,组件每次重新渲染导致都从新调用大型的计算函数,这是非常消耗性能的,我们可以使用 useMemo 来缓存这个函数的计算结果,来减少 JavaScript 在呈现组件期间必须执行的工作量,来缩短阻塞主线程的时间。

// 只有当 id 发生变化的时候才会从新计算
const TestComponent = () => {
  const value = useMemo(() => {
    return expensiveCalculation()
  }, [id]) 
  return <Component countValue={value} />
}

在使用 useMemo 缓存计算结果之前,还需要在适当的地方应用,useMemo 也是有成本的,它也会增加整体程序初始化的耗时,除非这个计算真的很昂贵,比如阶乘计算。

所以并不适合全局使用,它更适合做局部的优化。不应该过度 useMemo。

另外在缓存结果值的同时,还可以用来缓存组件。

比如有一个全局 context ,随着长期项目迭代 context 里面塞了很多状态,我们知道,context 的 value 发生变化,就会导致组件的重新渲染,而这个组件时一个很消耗性能的大型组件,只会被其中一个变量所影响才重新渲染,这时候就可以考虑使用 useMemo 进行缓存。

const TestComponent = () => {
  const appContextValue = useContext(AppContext);
  const theme = appContextValue.theme;
  return useMemo(() => {
    return <RootComponent className={theme} />;
  }, [theme]);
};

<RootComponent /> 只有在 theme 变量发生变化的时候重新渲染。

使用React.Fragment片段

react 有规定组件中必须有一个父元素,但是在某些情况下,根标签不需要任何的属性,这会导致整个应用程序内创建许多无用的元素,那么这个标签的作用并没有太大的意义。

const TestComponent = () => {
  return (
    <div>
      <ChildA />
      <ChildB />
      <ChildC />
    </div>
  );
}

实际上页面上的元素越多,DOM结构嵌套越深,加载所需的时间就越多,也会增加浏览器的渲染压力。

因此 React 提供了 Fragment 组件来代替包裹外层,它不会帮我们额外的创建外层 div 标签。

const TestComponent = () => {
  return (
    <React.Fragment>
      <ChildA />
      <ChildB />
      <ChildC />
    </React.Fragment>
  );
}

或者另一种简洁的方式使用空标签 <></> 代替也是一样的效果:

const TestComponent = () => {
  return (
    <>
      <ChildA />
      <ChildB />
      <ChildC />
    </>
  );
}

另外还有一些实用的场景,根据条件渲染元素

const TestComponent = () => {
  const { isLogin, name } = useApp();
  return (
    <>
      {isLogin ? (
        <>
          <h>Welcome {name}</h3>
          <p>You are logged in!</p>
        </>
      ) : (
        <h>go login...</h3>
      )}
    </>
  );
};

组件懒加载

应用程序初始化加载的快慢也跟组件的数量有关,因此在初始化的时候,一些我们看不见的页面,也就是最开始用不到的组件可以选择延迟加载组件,我们可以想到的是路由的懒加载,这样来提升页面的加载速度和响应时间。

react 提供了 React.Lazy 和 React.Suspense 来帮我们实现组件的懒加载。

import React, { lazy, Suspense } from 'react';
const AvatarComponent = lazy(() => import('./AvatarComponent'));
const renderLoader = () => <p>Loading</p>;
const DetailsComponent = () => (
  <Suspense fallback={renderLoader()}>
    <AvatarComponent />
  </Suspense>
)

Suspense 作用就是弥补在 Lazy 组件加载完成之前这段空白时间所能做的事情,尤其在组件较大,或者在较弱的设备和网络中,就可以通过 fallback 属性添加一个 loading 提示用户正在加载的状态。异步组件加载完成之后就会显示出来。

如果单独使用 lazy React 会在控制台发出错误提示!

通过 CSS 加载和卸载组件

渲染是昂贵的,如果频繁加载/卸载‘很重’的组件,这个操作可能非常消耗性能或者导致延迟。正常情况下,我们都会用三元运算符在判断加载显示,也导致了一个问题,每次频繁更新,触发加载不同的组件,就会有一定的性能损耗。这时我们可以使用 CSS 属性将其隐藏,让 DOM 能够保留在页面当重。

**不过这种方式并不是万能的,可能会导致一些布局或者窗口发生错位的问题。**但我们应该选择在不是这种情况下使用调整CSS的方法。另外一点,将不透明度调整为0对浏览器的成本消耗几乎为0(因为它不会导致重排),并且应尽可能优先于更该visibility 和 display。

// 避免对大型的组件频繁对加载和卸载
const ViewExample = () => {
  const [isTest, setIsTest] = useState(true)
  return (
    <>
      { isTest ? <ViewComponent /> : <TestComponent />}
    </>
  );
};
// 使用该方式提升性能和速度
const visibleStyles = { opacity: };
const hiddenStyles = { opacity: };
const ViewExample = () => {
  const [isTest, setIsTest] = useState(true)
  return (
    <>
      <ViewComponent style={!isTest ? visibleStyles : hiddenStyles} /> 
			<TestComponent style={{ isTest ? visibleStyles : hiddenStyles }} />
    </>
  );
};

变与不变的地方做分离

通常使用 useMemo、useCallback 进行优化,这里说说不借助这些Hooks进行优化,

变与不变做分离的概念来源,其实就是因为自身的react 的机制,父组件的状态更新了,所有的子组件得跟着一起渲染,意思是将有状态的组件和无状态的组件分离开。

function ExpensiveCpn() {
  console.log("ExpensiveCpn");
  let now = performance.now();
  while (performance.now() - now <) {}
  return <p>耗时的组件</p>;
}
export default function App() {
  const [num, updateNum] = useState("");
  return (
    <>
      <input
        type="text"
        onChange={(e) => updateNum(e.target.value)}
        value={num}
      />
      <ExpensiveCpn />
    </>
  );
}

上面输入框输入都会刷新组件<ExpensiveCpn/>,我们可以不使用 useMemo 等API就能控制渲染其实就是将变得和不变的分离开👇🏻:

function ExpensiveCpn() {
  console.log("ExpensiveCpn");
  let now = performance.now();
  while (performance.now() - now <) {}
  return <p>耗时的组件</p>;
}
function Input() {
  const [num, updateNum] = useState("");
  return (
    <input
      type="text"
      onChange={(e) => updateNum(e.target.value)}
      value={num}
    />
  );
}
export default function App() {
  return (
    <>
      <Input />
      <ExpensiveCpn />
    </>
  );
}

这样渲染的组件只会是 <Input/>组件内部,不会影响到外部。

总结

上面一些方式,可以从几个方面理解:

  • 减少重新render的次数:memo、useMemo、useCallback 使用、避免使用内联对象、变与不变的分离。
  • 减少渲染的节点:React.Fragment 片段、组件懒加载。
  • 降低渲染计算量:遍历试图使用 key。