React 日常开发中如何编写组件能提高它的性能

110 阅读6分钟

前言

我们现在刚开始学习React,接下来可能就要开始做项目,那日常开发中,我们经常需要把一个页面要拆分成多个组件, 通过拼装嵌套的方式来实现整体的页面效果,所以与其说去优化 React,不如聚焦在现有的组件中,思考🤔如何去设计一个组件才能提高他的性能,从而提高整个项目的性能以及交互的流畅性。

思考?
import React, { Component, useState } from "react";
const App = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h2>App: count: {count}</h2>
      <HelloWorld></HelloWorld>
      <button onClick={(e) => setCount(count + 1)}> +1 </button>
    </div>
  );
};

class HelloWorld extends Component {
  render() {
    console.log("子组件渲染");
    return <div>HelloWorld</div>;
  }
}
export default App

我们看下这段代码,就是很简单的父组件嵌套子组件,大家有发现什么特别的地方吗,我们其实很容易的观察出来,每当我点击+1, 我们的子组件HelldWorld都会被渲染一次,但是我明明改变的父组件的count,我子组件完全没有任何地方有用到count,那能不能想办法优化一下,就是你在点击+1的时候,我子组件不进行渲染。

性能优化实践

由于 React 中的组件分为 Function(函数) 组件和 Class (类)组件,优化的手段根据其特性不同也分为两类。

Function(函数) 组件

memo

memo – React 中文文档 (docschina.org)

React.memo 是 React 提供的一个高阶组件,用于优化组件的性能。它可以在某些情况下避免不必要的组件重新渲染,从而提高应用程序的性能。其使用方式分为两种:

  • 基础使用 函数组件直接包裹 memo 默认使用浅层比较。
  • 高阶使用 如果需要更精确地控制何时重新渲染组件,可以通过传递第二个参数给 memo 来指定自定义的比较函数。这个比较函数接收两个参数,分别是前一次的 props 和当前的 props ,返回一个布尔值表示是否需要重新渲染组件。
import React, {memo} from 'react'; 
const areEqual = (prevProps, nextProps) => { 
// 自定义比较逻辑 
// 返回 true 表示两个 props 相等,不需要重新渲染 
// 返回 false 表示两个 props 不相等,需要重新渲染 return prevProps.value === nextProps.value; }; 

const MyComponent = memo((props) => { 
console.log('Rendering MyComponent'); 
return <div>{props.value}</div>; 
}, areEqual);

useEffect

useEffect – React 中文文档 (docschina.org)

官方说法:useEffect 是一个 React Hook,它允许你 将组件与外部系统同步。 这么说很抽象,实际上我们可以把 useEffect 叫做副作用处理函数。它拥有生命周期特性,組件在更新或卸载之前会回调这个函数。我们可以在 useEffect 函数中,发送网络请求、监听、store.subscribe(卸载)

import React, {memo} from 'react'; 
const HeaderRight = memo(() => {
  // 定义状态
  const [showPanel, setShowPanel] = useState(false);
  // 副作用处理函数
  // 当前传入的回调函数会在组件渲染完成后,自动执行
  useEffect(() => {
    const windowHandleClick = ()=> {
      setShowPanel(false);
    }
    window.addEventListener('click', windowHandleClick, 
    //设置捕获
    true);
    // 取消监听
    return ()=> {
      window.removeEventListener('click', windowHandleClick, true)
    }
  }, []);
  // 事件处理函数
  const panelHandleClick = () => {
    setShowPanel(true);
  };
  return (
    <div>
      {showPanel && (
        <div className="panel">注册</div>
      )}
    </RightWrapper>
  );
});

export default HeaderRight;
useCallback

useCallback 是 React 中的一个 Hook,用于优化性能和避免不必要的渲染。它主要用于创建一个稳定的回调函数,并在依赖项未发生变化时缓存该函数。

import React, { memo, useCallback, useRef, useState } from "react";

// memo 允许你的组件在 props 没有改变的情况下跳过重新渲染。
const HYHome =  memo((props)=> {
  const {increment} =  props
  return (
    <div>
      <button onClick={e => increment()}>修改message HYHome</button>
      {/* 100个子组件 */}
    </div>
  )
})
const App = memo(() => {
  const [counter, setCounter] = useState(1);
  const [message, setMessage] = useState("Hello");

  // const increment = e=> {setCounter(counter + 1)}
  // 当一个函数需要给多个组件使用,我们可以使用useCallback进行性能优化,只有当特定值发生改变时,方法才会重新执行
  // 闭包陷阱
  const increment = useCallback(
    // 回调函数
    function increment() {
      setCounter(counter + 1);
      console.log("渲染");
    },
    // 依赖值
    [counter]
  );
  // 进一步优化: counter 改变时也使用同一个函数 
  // 方法1: 将counter直接移除:缺点: 闭包陷阱, increment只会加1 ,之后不会改变
  // 方法2: useRef,在组件多次渲染后,返回的是同一个值
  // const counterRef = useRef()
  // counterRef.current = counter
  // const increment = useCallback(
  //   // 回调函数
  //   function increment() {
  //     setCounter(counterRef.current + 1);
  //     console.log("渲染");
  //   },
  //   // 依赖值
  //   []
  // );
  // 每次渲染都会被定义一次

  return (
    <div>
      <h2>当前计数; {counter}</h2>
      消息: {message}
      <button onClick={increment}>+ 1</button>
      <button onClick={(e) => setMessage(Math.random())}>修改message</button>
      <HYHome increment={increment}></HYHome>
    </div>
  );
});

export default App;
useMemo

它用于在组件渲染过程中进行记忆化计算,以避免不必要的重复计算,提高应用的性能。 使用场景:

  • 计算昂贵的计算结果:涉及到需要执行昂贵的计算或处理大量数据的情况下,可以使用 useMemo 将计算结果缓存起来
  • 避免不必要的渲染:某个组件的渲染结果仅依赖于特定的输入参数,并且这些参数没有发生变化时,可以使用 useMemo 缓存该组件的输出,避免不必要的重新渲染
import React, { memo,  useMemo, useState } from "react";

const calcNumber = (num) => {
  console.log("calcNumber渲染");
  let total = 0;
  for (let i = 0; i < num; i++) {
    total += i;
  }
  return total;
};

const HelloWord = memo(() => {
  console.log('hell word')
  return <div>Hello Word</div>;
})
// let total = calcNumber(50)
const App = memo(() => {
  const [counter, setCounter] = useState(0);
  // 1.不依赖任何的值,进行计算
  let total = calcNumber(50)
  // let total = useMemo(() => {
  //   return calcNumber(50);
  // }, []);

  // 2.依赖counter
  // let total = useMemo(() => {
  //   return calcNumber(counter * 2);
  // }, [counter]);

  // function fn () {}
  //3. useCallback和useMemo 比较
  // 返回有记忆的函数
  // const result = useCallback(fn, []);
  // 返回有记忆的值  对函数的返回值做优化
  // const result = useMemo(() => fn, []);


  // 4.使用useMemo对子组件渲染进行优化
  const info = useMemo(()=> ({ age: 18, name: 'why'}), [])
  return (
    <div>
      <h2>计数: {counter} </h2>
      <button onClick={(e) => setCounter(counter + 1)}>+ 1</button>
      {/* <h2>计算结果: {calcNumber(50)}</h2> */}
      <h2>{total}</h2>
      <HelloWord total={total} info={info}/>
    </div>
  );
});

export default App;
Class (类)组件
PureComponent

!!仅支持React 15.3及以上版本 PureComponent 是继承自 React.Component 的一个子类,它额外实现了shouldComponentUpdate 方法,并通过对组件的 props 和 state 进行浅层比较来确定是否需要重新渲染组件。如果 props 和 state 没有发生改变,就不会执行 render 函数,省去了生成 Virtual DOM 和 Diff 的过程。

合理使用shouldComponentUpdate

如果想对渲染进行更加细微的控制,或者是对引用类型进行渲染控制我们可以使用shouldComponentUpdate,通过返回 true  进行更新, false 阻止不必要的更新。

 shouldComponentUpdate (nextProps, nextState) {
    if(this.state.message !== nextState.message || this.state.counter !== nextState.counter) {
      return true
    } else {
      return false
    }
  }

特别注意⚠️:合理使用,手动实现 shouldComponentUpdate 可能会增加代码的复杂性,并且过度使用它可能会导致更多的维护问题。只有在确实需要优化性能时,才建议使用它。

使用lazy进行组件懒加载

过组件懒加载可以将代码分割成更小的块,并且只有在需要时才会被加载 当用户访问某个特定页面时,只有与该页面相关的代码会被下载和执行,而其他代码则不会被加载。这样可以使应用程序更快地启动,并减少页面响应延迟。通常我们在编写路由的时候会使用到懒加载

import React from "react";
// 懒加载 打包出来是单独的
const About  = React.lazy(()=>import('../pages/About'))
const Login  = React.lazy(()=>import('../pages/Login'))
// 这些都是同步加载,没有做分包处理
const routes = [
  {
    path: "/",
    element: <Navigate to="/home" />,
  }
]
import React, { Suspense } from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { HashRouter } from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <HashRouter>
      {/* 懒加载的组件 */}
      <Suspense fallback={<h3>Loading</h3>}>
        <App />
      </Suspense>
    </HashRouter>
  </React.StrictMode>
);