100万件データをサーバーから取得してブラウザに表示させるのにSpring WebFlux使ったら初期描画が速くなると思ったのでやってみた

先日、「[Spring WebFluxのFlux結果をJSからFetch APIで取得する](https://qiita.com/ota-meshi/items/778c6830ec878e729227)」という記事を書きましたが、この方法を使って、
例えば、大量のデータをブラウザに表示させたい時に、サーバーで最初の数10件だけ処理できたら、その数10件だけ先にブラウザに表示させて、その後処理できたデータをバックグラウンドでちょろちょろ追加していく。みたいなことが**簡単に**できそうだなと思ったので試してみました。

ユーザーに大量のデータを要求された場合、ページングで実装するという選択肢もありますが、
こういう選択肢もあるかもねー。程度の記事です。この方法をオススメするわけではありません。

ここで試したものは[GitHub](https://github.com/ota-meshi/spring-webflux-x-cheetah-grid-example)にアップしています。

# 処理内容

100万件のデータをサーバーに要求して、結果をブラウザのグリッドに表示します。

## 使ってる技術要素

- サーバーサイド
  - Java11
  - Spring WebFlux
  - [H2 Database](https://www.h2database.com/html/main.html)  
    データベース＆jdbc。ガチでDBサーバー立てるのめんどくさいのでH2でファイルDBにしました。
  - [uroboroSQL](https://github.com/future-architect/uroborosql)  
    DBアクセスライブラリ。弊社のOSS。何使っても良かったのですけど、私これに慣れてるのでこれ使います。
- クライアントサイド
  - [Vue.js](https://vuejs.org/)  
    何使っても良かったのですけど、私がVue好きなのと、[Vue CLI](https://cli.vuejs.org/)使うと簡単に構築出来てとても好きなので使います。
  - [Cheetah Grid](https://github.com/future-architect/cheetah-grid)  
    グリッドライブラリ。自分が作った弊社のOSS。100万件表示できるように作ったつもりなのでこれ使います。

# 比較と動画

先に比較結果をお見せしておきます。

## Spring WebFluxを利用した非同期リクエスト

最初に描画されるまでの時間は2,056msでした。
もう少し早いかなーと思ったのですが、H2 DatabaseでSQLクエリを発行してから1件目が返るまでに時間がかかるみたいですね。（まあH2はプロダクション用途ではないと思うので仕方ないです。）
動画は切ってしまっていますが、その後、チョロチョロデータを取得して、100万件をクライアント側に溜め込むまでに40秒ぐらいかかります。

![WebFlux.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84259/722cd3fd-d87c-7b85-2be3-60dcb61dfdcb.gif)

## 普通の同期リクエスト

描画されるまでの時間は5,827msでした。
一度に全部持ってきてるので、100万件をクライアント側に溜め込むまでの時間という意味でも同じ5,827msです。
もっと遅くなると思っていましたが、各レコードの構築で何か処理をしているわけでもなく、1レコードの情報も少ないので意外と速かったというところでしょうか？

![sync.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84259/c2208c4b-39ea-de35-3b96-f6cfca4f9458.gif)

# 実装

以下で実装についても書いておきます。

## Controller（サーバーサイド）

以下のような実装になっています。

```java
	@Autowired
	private SqlConfig uroboroSQLConfig;

	@Autowired
	@Qualifier("jdbcScheduler")
	private Scheduler jdbcScheduler;

	@GetMapping
	public Flux<Person> all() {
		return Flux.<Person> create(sink -> {
			try (var agent = uroboroSQLConfig.agent()) {
				// テーブルをSELECTしてPersonクラスにMappingしたStreamを生成
				agent.queryWith("SELECT * FROM PERSON ORDER BY ID")
						.stream(Person.class)
						// キャンセルされていたら終了させる
						.dropWhile(p -> sink.isCancelled())
						// FluxSinkにEntityを渡す
						.forEach(sink::next);

				if (!sink.isCancelled()) {
					// 終了を通知
					sink.complete();
				}
			}
		}).subscribeOn(jdbcScheduler);
	}
```

GitHubに上げてあるソースだと[このあたり](https://github.com/ota-meshi/spring-webflux-x-cheetah-grid-example/blob/master/src/main/java/example/spring/webflux/controller/PersonController.java#L27)です。

`uroboroSQLConfig`というのは[uroboroSQL](https://github.com/future-architect/uroborosql)のDBアクセスで必要なものです。本題とは関係ないので気にせず、DBアクセス用の何かなんだなーと思って空気読んでいただければと思います。

`jdbcScheduler`は[Reactor](https://projectreactor.io/)の`Scheduler`インスタンスで、並列化に必要なものです。

適切なスレッド数で`Scheduler`を定義して、

```java
		}).subscribeOn(jdbcScheduler);
```

と渡すことで、DBアクセスの処理と、リクエストを返す処理を並列化します。

あとは、キャンセル処理とかもしていますが、
基本的にはDBから`SELECT`してその結果を`sink.next(...)`で渡していくだけです。

## リクエスト（クライアントサイド）

[Spring WebFluxのFlux結果をJSからFetch APIで取得する](https://qiita.com/ota-meshi/items/778c6830ec878e729227)で書いた方法で、[Fetch API](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API)を使ってサーバーにリクエストします。

## グリッドに表示（クライアントサイド）

以下のような実装になっています。

```js
      const records = [];
      const grid = /* Cheetah Gridのインスタンス */;
      let buffer = [];
      //...
      streamJsonForVue(this, "/api/persons", {}, rec => {
        buffer.push(rec);
        if (
          buffer.length >= 10000 ||
          (records.length < 10000 && buffer.length >= 1000) ||
          (records.length < 1000 && buffer.length >= 100)
        ) {
          records.push(...buffer);
          grid.records = records;
          buffer = [];
          //...
        }
      }).then(() => {
        records.push(...buffer);
        grid.records = records;
        //...
      });
```

GitHubに上げてあるソースだと[このあたり](https://github.com/ota-meshi/spring-webflux-x-cheetah-grid-example/blob/master/src/components/SampleCheetahGrid.vue#L100)です。

`grid`変数に[Cheetah Grid](https://github.com/future-architect/cheetah-grid)のインスタンスが入っていて、`records`プロパティに配列を与えると、その与えたデータが画面に表示されます。

`streamJsonForVue`関数は中で、「[Spring WebFluxのFlux結果をJSからFetch APIで取得する](https://qiita.com/ota-meshi/items/778c6830ec878e729227)」に書いた、[Fetch API](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API)の処理をしています。
プラスで、画面が破棄された時に、Fetchを中断するとかやってますが、本題には関係ないので説明は割愛します。

`streamJsonForVue`のcallbackから返ってきた、`rec`変数はControllerで`sink.next(...)`で渡された1レコードデータです。
１件ずつ`grid`に渡してしまうと、再描画が走りまくってユーザーが画面を操作しにくくなるため、`buffer`配列に溜め込んで、ある程度溜まったら（最初は100件）、gridに反映させるようにしています。

## 同期リクエストの方の実装

GitHubに上げてあるソースだと[このあたり](https://github.com/ota-meshi/spring-webflux-x-cheetah-grid-example/blob/master/src/main/java/example/spring/webflux/controller/PersonController.java#L48)と[このあたり](https://github.com/ota-meshi/spring-webflux-x-cheetah-grid-example/blob/master/src/components/SampleCheetahGrid.vue#L131)です。
比較用に作ってみただけで、本題には関係ないので説明は割愛します。

## 実装した感想

私が実装してみて思ったこととしては、実装が**とても簡単だった**ということです。
自分の知識だと、HTTPレスポンスを非同期でStreamに流すのは難しいイメージでしたが、Spring WebFlux使うととても簡単に出来たのでびっくりしています。

# 結果

先にも書きましたが、今回のサンプルでは、初期描画にかかる時間はそれぞれ以下の結果になりました。

- Spring WebFluxを利用した非同期リクエスト  
  2,056ms
- 普通の同期リクエスト  
  5,827ms

今回のサンプルでは、思っていたより効果が出なくて残念でしたが、どうしても初期描画だけは速くしたいような場合に、もしかしたら使えるかもしれません（？）