banner
innei

innei

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

React アプリケーションにおけるパフォーマンス最適化の経験(2)

上回说道在 React アプリケーションでリストコンポーネントは最適化すべきで、今日は複雑なコンポーネントをどう書くかについてです。Jotai と Zustand が咕咕咕したので、次回にします。

大手企業の屎山を経験したことがある人は、コンポーネントが数百行、さらには千行を超えることがよくあることを知っているでしょう。コンポーネント内部に別のコンポーネントをネストするのも一般的です。簡単に三つの「しないこと」をまとめました:

  • コンポーネント内部で他のコンポーネントを定義しない
  • render の方法で ReactNode をレンダリングしない、例: 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>
    )
}

以上は非常に単純な例ですが、こうした誤りを犯す人はいないでしょう。なぜなら、このコンポーネントは非常にシンプルで、簡単に分割できるからです。どうしてこんなに初心者のコードを書くことができるのでしょうか。実際のビジネスシーンでは、コンポーネントの複雑さはこれよりもはるかに大きく、時にはロジックを抽出することができず、こうした初心者のコードを書くしかないこともあります。その結果、すべての状態がコンポーネントのトップレベルにあり、コンポーネント内のコンポーネントもトップレベルコンポーネントの再レンダリングによって常に再構築され、パフォーマンスが非常に低下します。

コンポーネント内部での他のコンポーネントの定義を排除する#

少し複雑な、データが結合されたビジネスコンポーネントを定義しなければならない場合、コンポーネント内に位置するコンポーネントを使用する必要があるかもしれません。そして、それを最適化します。

例えば、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 が再マウントされることになります。React では、新しいコンポーネントインスタンス(つまりコンポーネントの初回レンダリング)を作成することは、既存のコンポーネントインスタンス(つまりコンポーネントの再レンダリング)を更新するよりも通常は時間がかかります。

正しい方法は、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 コンポーネントが再レンダリングされた後、すべての列の 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

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