iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

react 無限スクロールを作ってみた

はじめに

 こんにちは、iimon新卒エンジニアのみやこしです、最近業務でreactを使っている最中にAPIから配列のデータをfeatchして、それをテーブルに表示する際に大量のコンポーネントが作られ、パフォーマンスが悪くなりました。それを解決するために、色々探ったところ、無限スクロールというものを知り、それについて調べてみました!

無限スクロールとは

Slackのように画面のページの下部に近づくと自動的に新しいコンテンツを読み込む機能です。今回は無限スクロールの機能を提供するライブラリーであるreact-windoを使って実装します!

react-windowとは

react-windowは、パフォーマンスを最適化するために仮想リスト技術を使用するnpmパッケージです。例えばあなたはレンダリングしたいデータが10万件あるとします、パフォーマンスを気にしなければ一度にレンダリングすることもできます。しかし、ほとんどの場合では、スクリーンの表示領域で10万件のデータは表示することはできません。つまり、ほとんどのデータは可視領域の外にありることになります。だから見える範囲のデータだけレンダリングすればいいのです。

引用:リアクション ウィンドウを使用して大きなリストを仮想化する  |  Articles  |  web.dev

このようにreact-windowは見える範囲のコンポーネントレンダリングし、見えない部分はDOMノードに回収されます。

react-window使ってみよ

インストール: 以下のコマンドで簡単にダウンロードすることができます!

npm i react-window

react-windowは以下の4つのコンポーネントがあります

今回はFixedSizeListを使って無限スクロールの実装をします。

基本的な使い方

公式から:

import { FixedSizeList as List } from 'react-window';

const Row = ({ index, style }) => (
  <div style={style}>Row {index}</div>
);

<List
  height={400}
  width={300}
  itemSize={50} // 各アイテムの高さが固定
  itemCount={1000}//アイテムの数
>
  {Row}
</List>

※ react-windowは画面外のアイテムはレンダリングされないため、style を適用することで、本当に必要なアイテムだけを適切な位置に表示する仕組みになっているので必須になります。

APIからのデータを使った例

以下のダミーデータを使って無限スクロールを作りました!

//ダミーデータ
const items = Array.from({ length: 500 }, (_, index) => ({
  name: "iimonくん",
  id: index + 1,
  address: "tokyo",
}));
const itemsData = {
  items: items,
  //親コンポーネントからのpropsなどを受け取る
};

// 1行ごとのコンポーネント
const Row = ({ index, style, data }) => {
  const item = data.items[index];

  const additionalStyle = {
    padding: "10px",
    width: "300px",
    height: "50px",
    borderBottom: "1px solid #ccc",
  };

  return (
    <div style={{ ...style, ...additionalStyle }}>
      <strong>Name:</strong> {item.name}, <strong>Address:</strong>{" "}
      {item.address}
    </div>
  );
};

const DummyList = () => {
  return (
    <List
      height={400} // リスト全体の高さ
      width={400} // リスト全体の幅
      itemSize={35} // 各アイテムの高さ
      itemCount={items.length} // アイテムの数
      itemData={itemsData} // アイテムのデータ
    >
      {Row}
    </List>
  );
};

export default DummyList;

itemDataはRowコンポーネントに必要なデータを渡すことがでます、今回は例としてダミーデータを渡していますが、実際親コンポーネントから受け取ったpropsや関数などもitemDataに渡すことがあります。

Profilerを使ってレンダリングの時間を計測してみた

実際react-windowを使わない時と使う時のレンダリングの時間を比較してみました! 今回はReact DevToolsが提供しているProfilerタブを使いました。

使い方:

import ReactDOM from "react-dom/client";
import List from "./react-window/App";
import React, { Profiler } from "react";

const root = ReactDOM.createRoot(document.getElementById("root"));

const onRenderCallback = (
  id, // プロファイルされているコンポーネントのID
  phase, // "mount"(初回マウント)か "update"(更新時)
  actualDuration // コンポーネントのレンダーにかかった時間
) => {
  console.log(`レンダーでかかった時間: ${actualDuration}ms`);
};

root.render(
  <Profiler id="List" onRender={onRenderCallback}>
    <List />
  </Profiler>
);

分析したいコンポーネントにProfilerでラップすると簡単に計測することができます!✨

以下はreact-windowを使わな時のコードになります

const DummyList = () => {
  return (
    <div>
      <ul>
        {items.map((item) => (
          <li
            key={item.id}
            style={{
              padding: "10px",
              width: "300px",
              height: "50px",
              border: "1px solid #ccc",
              listStyle: "none",
            }}
          >
            <strong>Name:</strong> {item.name}, <strong>Address:</strong>{" "}
            {item.address}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default DummyList;

react-window使わないと、レンダリングする時間は約11.4ミリ秒かかることがわかります

react-window使うと、レンダリングする時間はなんと約2ミリ秒です!!!

最後に

ここまで読んでくださりありがとうございます!普段何気なく使っているコンポーネントの描画が、どのように最適化したらいいのか理解できました。Profiler を使った計測方法を実際に試すことで、レンダリングのコストを可視化し、今後パフォーマンスを気にして実装することを学びました この記事を読んで興味を持って下さった方がいらっしゃれば、カジュアルにお話させていただきたいです。是非ご応募をお願いいたします!

Wantedly / Green

参考文献

react-window で巨大なリストを低コストに表示する #React - Qiita

react-window

<Profiler> – React