📘

react-selectの性能問題をreact-windowで解決する

2022/11/01に公開

react-selectで大量(数千件〜)の選択肢を扱おうとすると、急激にブラウザの処理が遅くなります。
この問題に関するIssueはすでに作成されており、様々な解決法が提示されています。
https://github.com/JedWatson/react-select/issues/3128

本記事では、react-windowを利用した解決策を提示します。

問題の再現

以下のようなシンプルなコードで再現できます。
PCの性能によって性能劣化の程度が異なりますので、optionsの件数を適宜増減してください。

import Select from "react-select";

const options = [...Array(10000)].map((_, i) => ({
  value: i,
  label: `label ${i}`
}));

function App() {
  return <Select options={options} />;
}

解決策

react-windowを用いた解決策を紹介します。
なお、react-windowはリスト表示の性能向上を目的としてよく利用されるライブラリです。

react-selectではUIを柔軟にカスタマイズできるようになっています[1]
今回は、選択肢の表示部分であるMenuListコンポーネントのカスタマイズを行います。

import Select, { GroupBase, MenuListProps } from "react-select";
import { FixedSizeList } from "react-window";

type Option = {
  value: number;
  label: string;
};

const options: Option[] = [...Array(10000)].map((_, i) => ({
  value: i,
  label: `label ${i}`
}));

const MENU_LIST_ITEM_HEIGHT = 35;

function MenuList({
  options,
  getValue,
  maxHeight,
  children
}: MenuListProps<Option, false, GroupBase<Option>>) {
  if (!Array.isArray(children)) {
    return null;
  }

  const [selectedOption] = getValue();
  const initialScrollOffset =
    options.indexOf(selectedOption) * MENU_LIST_ITEM_HEIGHT;

  return (
    <FixedSizeList
      width="auto"
      height={maxHeight}
      itemCount={children.length}
      itemSize={MENU_LIST_ITEM_HEIGHT}
      initialScrollOffset={initialScrollOffset}
    >
      {({ index, style }) => <div style={style}>{children[index]}</div>}
    </FixedSizeList>
  );
}

function App() {
  return (
    <Select<Option, false, GroupBase<Option>>
      options={options}
      components={{ MenuList }}
    />
  );
}

補足

この問題のIssueでは、選択肢の要素につけられている onMouseOveronMouseMove が性能劣化の原因であるとして、これらのイベントハンドラを削除する方法も提案されています。
これを行うと確かにある程度の効果は得られますが、選択肢にマウスカーソルをあてた時にハイライトされる機能が無効化されます。
利用者体験が下がってしまうこと、react-windowの導入で性能問題が解消されることを考えると、この方法をとる必然性は無いように感じます。

脚注
  1. https://react-select.com/components ↩︎

Discussion