banner
innei

innei

写代码是因为爱,写到世界充满爱!
github
telegram
twitter

React 应用中性能优化的经验(二)

上回说道在 React 应用中列表组件应该去优化,今天复杂组件应该怎么写。Jotai 和 Zustand 咕咕咕了,下次再说。

写过大厂屎山的大伙应该都经历过,一个组件能有上百甚至上千行都是常事。一个组件内部嵌套一个组件也是常事。简单总结了下三不要:

  • 不要在 Component 内部定义其他 Component
  • 不要用 render 的方式渲染 ReactNode, eg. render(data1, data2, data3) => ReactNode
  • 不用 useCallback 定义一个组件

啥意思,简单列举一下上面的错误写法,大家千万不要学:

// ❌ 在组件内部定义其他组件
function ParentComponent(props) {
    function ChildComponent() {
        return <div>我是子组件</div>
    }

    return (
        <div>
            <ChildComponent />
        </div>
    )
}

// ❌ 用 render 的方式渲染 ReactNode:
function MyComponent({ data1, data2, data3 }) {
    const render = (data1, data2, data3) => {
        return <div>{data1}{data2}{data3}</div>
    }

    return render(data1, data2, data3);
}

// ❌ 用 useCallback 定义一个组件
import React, { useCallback } from 'react';

function ParentComponent(props) {
    const ChildComponent = useCallback(() => {
        return <div>我是子组件</div>
    }, []);

    return (
        <div>
            <ChildComponent />
        </div>
    )
}

以上只是非常简单的例子,这样的错误肯定大家都不会犯,因为这个组件很简单,很好拆,怎么会写出这么新手的代码呢。在实际业务场景中,组件复杂度比这个大得多,有的时候好像没有办法去抽离一些逻辑就只能写出这种新手代码了,导致所有的状态全在组件顶层,组件中的组件也会因为顶层组件的 re-render 一直重建,性能是非常低下的。

消除组件内部定义的其他组件#

我们定义一个稍微复杂一些的,数据耦合在一起的好像不得不使用组件中定位组件的一个业务组件,然后对它进行优化。

假设我们有一个OrderForm组件,它需要根据传入的订单信息动态生成不同的OrderItem组件。这个时候,你可能会想在OrderForm中定义OrderItem组件,如下:

错误写法:

function OrderForm({ orders }) {
  	const [myName, setMyName] = useState('Foo')
    function OrderItem({ order }) {
        return (
            <li>
                <h2>{order.name}</h2>
                <p>数量: {order.quantity}</p>
                <p>单价: {order.price}</p>
                <p>{myName}</p>
            </li>
        )
    }
  	
   // 或者下面的形式
	  const OrderItem = React.useCallback(({ order }) {
        return (
            <li>
                <h2>{order.name}</h2>
                <p>数量: {order.quantity}</p>
                <p>单价: {order.price}</p>
				<p>{myName}</p>
            </li>
        )
    }, [order, myName])

    return (
        <ul>
            {orders.map(order => 
                <OrderItem key={order.id} order={order} />
            )}
        </ul>
    );
}

但是这样做,每次OrderForm渲染时,OrderItem都会被重建,也就意味着这个列表遍历下的所有OrderItem 都在 re-mount 而不是 re-render。在 React 中,创建一个新的组件实例(即组件的首次渲染)通常比更新一个现有的组件实例(即组件的 re-render)要花费更多的时间。

正确的做法是,将OrderItem组件定义在OrderForm外面,通过 props 传递数据:

function OrderItem({ order, myName }) {
    return (
        <li>
            <h2>{order.name}</h2>
            <p>数量: {order.quantity}</p>
            <p>单价: {order.price}</p>
            <p>{myName}</p>
        </li>
    )
}

function OrderForm({ orders }) {
    return (
        <ul>
            {orders.map(order => 
                <OrderItem key={order.id} order={order} myName={myName}/>
            )}
        </ul>
    );
}

这样,不管OrderForm如何渲染,OrderItem都只会被定义一次,而且它可以根据 props 的改变来决定是否需要重新渲染,从而提高性能。

去除 render 方式传参的组件#

有时会看到一些组件是这样定义插槽的。比如下面的 Table 的组件。

type Data = (string | number)[][];

type ColumnRenderer = (data: string | number, rowIndex: number, columnIndex: number) => React.ReactNode;

interface TableProps {
    data: Data;
    columnRenderers: ColumnRenderer[];
}

const Table: React.FC<TableProps> = ({ data, columnRenderers }) => {
    return (
        <table>
            <tbody>
                {data.map((row, rowIndex) => (
                    <tr key={rowIndex}>
                        {columnRenderers.map((render, columnIndex) => (
                            <td key={columnIndex}>
                                {render(row[columnIndex], rowIndex, columnIndex)}
                            </td>
                        ))}
                    </tr>
                ))}
            </tbody>
        </table>
    );
};

上面的写法也不是很好,首先是 render 是一个函数返回的一个 ReactNode,所以你在用这个组件的时候,传入的 render 是不能使用 React Hooks 的。例如:

<Table data={[]} columnRenderers={[
    (data, index) => {
      useState() // ❌ 不能使用
      return <div>{data.id}</div>
  	}
]}

不仅不能使用 Hooks,而且这样的写法会导致 Table 组件 re-render 后,下面的所有列的 render 全部重建。因为 render 不是一个组件而是一个 ReactNode,不能用作性能优化。

上述的 Table 可以改写成:

type Data = (string | number)[][];

interface TableColumnProps {
    data: string | number;
    render: (data: string | number) => React.ReactNode;
}

interface TableProps {
    data: Data;
    columnComponents: Array<React.FC<TableColumnProps>>;
}

const Table: React.FC<TableProps> = ({ data, columnComponents }) => {
    return (
        <table>
            <tbody>
                {data.map((row, rowIndex) => (
                    <tr key={rowIndex}>
                        {columnComponents.map((Component, columnIndex) => (
                            <Component key={columnIndex} data={row[columnIndex]} />
                        ))}
                    </tr>
                ))}
            </tbody>
        </table>
    );
};

这样就可以在定义列组件时使用 Hooks 了并且列组件只受到 data 的影响(前提需要给列组件套上 memo),而且也只需要把原本的 (data1, data2) => 改写成 ({ data1, data2 }) 就行了。

如:

<Table data={[]} columnComponents={[
   ColumnComponent1
]}

const ColumnComponent1 = memo(({ data, index }) => {
      return <div>{data.id}</div>
})

今天先说到这。

上述部分代码和文字由 GPT-4 编写。

此文由 Mix Space 同步更新至 xLog
原始链接为 https://innei.ren/posts/programming/experience-in-performance-optimization-in-react-applications-2


読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。