Skip to content
On this page

calender ui scroll performance improvement

2022-10-27


散々こすらせてるカレンダー UI のメモもこれで最後になるか、まだまだ続くかはわからない。課題自体はたくさんあるが所詮これはプロトタイピングにすぎないので。

前回のメモ で触れたように、スクロール可能なカレンダーができた。
しかし、スクロールするときに、スクロールした分だけのリストを再描画するのは、パフォーマンス的にはよろしくない。
そのため、スクロールした分だけのリストを再描画するのではなく、更新時点のスクロール位置からプラスマイナス 1 年分だけのリストを作成し、setState して再描画するようにした。

ボトルネック

現在、具体的な数値を用いた計測は行なっていないため感覚での話になるが、自分の手元だと 50 年分ほど読み込むと挙動が怪しくなり、100 年分読み込んだあたりで壊れた。
壊れるというのは日付を選択した際の挙動が著しく遅くなることを指している。

このような挙動が起きるのは、スクロール位置からプラスマイナス 1 年分のリストを作成する際に、大量の日付の配列の中から selected な日付を探す処理が発生しているためである。
そのため、ただクリックを選択された日付を返すだけのハンドラーはどれだけカレンダーを読み込んでも問題なく動作することが確認できている。(これまた自分の手元だが)

今回は、ボトルネックを肥大化したリストと仮定して、カレンダーのクリックした際の挙動をサクサク動くことを目標に少しだけ改善を行なった。

Pull Request

例に倣って、また一応ブランチを切って作業をした。

demo

https://calender-ityak2bt9-takurinton.vercel.app で確認できる。

実装

まず、引数の日からプラスマイナス 1 年分のリストを作成する関数を作成した。
これは元々ロジックをべたで書いていたが、複数回呼び出す必要が出てきたので関数で区切ることにした。

引数でもらった日付をもとに、それぞれ、12 つの月のリストを作成する。

import { Dayjs } from "dayjs";

export const getNextYearMonthList = (date: Dayjs) =>
  Array.from(new Array(12)).map((_, i) => date.clone().add(i, "month"));

export const getPrevYearMonthList = (date: Dayjs) =>
  Array.from(new Array(12)).map((_, i) =>
    date.clone().subtract(12 - i, "month")
  );
1
2
3
4
5
6
7
8
9

次に、Calendar.tsx にて、scroll event を扱うハンドラーの中でのステート管理を変更した。 元々は、スクロールした分だけのリストを再描画するようにしていたが、今回は、スクロールした分だけのリストを作成し、setState して再描画するようにした。

以下は scrollUp のハンドラーの例。

従来の方法では、setMonthList で元のリストも含めたリストを生成していたが、今回の実装では新しく現在地からプラスマイナス 1 年分のみを生成して setMonthList することで扱うリストの長さを短くしている。

従来

if (scrollTop - MARGIN <= 0) {
  const prevYearMonthList = Array.from(new Array(12)).map((_, i) =>
    loaded.prev.clone().subtract(12 - i, "month")
  );
  setLoaded({ next: loaded.next, prev });
  // ここで既存の monthList を含めていたので何年も読み込むと動作が遅くなっていた
  setMonthList([...prevYearMonthList, ...monthList]);
}
1
2
3
4
5
6
7
8

今回

if (scrollTop - MARGIN <= 0) {
  const prevYearMonthList = getPrevYearMonthList(loaded.prev);
  const nextYearMonthList = getNextYearMonthList(loaded.prev);

  const next = loaded.next.subtract(1, "year");
  const prev = loaded.prev.subtract(1, "year");

  setLoaded({ next, prev });
  // スクロール位置からプラスマイナス1年分のみを読み込むことで、パフォーマンスが改善された
  setMonthList([...prevYearMonthList, ...nextYearMonthList]);
}
1
2
3
4
5
6
7
8
9
10
11

このようにすることで、更新が呼ばれた時点でのスクロール位置からプラスマイナス 1 年分のカレンダーをロードするので、ロード済みだが表示外の部分を過剰にロードすることがなくなった。
それ以外の部分は変わらず、同じ状態の持ち方をして表現している。

まとめ

割とサクサク動くようになったので、この方向でもいいと感じている。
一般的に使われるカレンダー UI は翌月の表示のトリガーが大体クリックだったりするが、今回はスクロールを用いて実装しているので考慮事項が若干異なる。
あまりボトルネックが増えないようにしつつ、効率の良い描画をしていくことが重要になる。まだまだ規模が小さく、機能が貧弱なので考えることが少ないが、今後はメモ化をしたり、パフォーマンス計測をしたりと楽しい検証が盛りだくさんになりそう。