はじめに
こんにちは、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:最も基本的のコンポーネントであり、固定サイズのリストを仮想化して表示するコンポーネントです。
VariableSizeList:可変サイズのリストを仮想化して表示するコンポーネントです。
FixedSizeGrid:固定サイズのグリッド(行と列)を仮想化するコンポーネントです。
VariableSizeGrid:可変サイズのグリッド(行と列)を仮想化するコンポーネントです。
今回は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 を使った計測方法を実際に試すことで、レンダリングのコストを可視化し、今後パフォーマンスを気にして実装することを学びました この記事を読んで興味を持って下さった方がいらっしゃれば、カジュアルにお話させていただきたいです。是非ご応募をお願いいたします!