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

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。