Blazor 大量データ表示を仮想化<Virtualize>して劇的に高速化にする

## １．大量データを効率よく表示したい
- あまり望ましくないですが、大量データを表示するUIが必要なケースもあると思います
- 数100行程度なら良いですが、数千、数万行となったらメモリ効率を考えたいです
:::note info
- Blazorの ```<Virtualize>``` タグを試したところ、**劇的に改善** したので共有します
:::


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/607ef0e7-1839-4575-8ac4-7228cb35f83e.png)


## ２．前提条件
### ２．１ 環境
* Visual studio 2022 Version 17.12.2
* .Net 8
* UI：Blazor Web App
* Blazor rendermode：InteractiveServerRenderMode

### ２．２ テストデータ
- Weatherテーブル：**50,000行** を全行表示
（ 列は No, 日付, 気温℃, 概要 ）

## ３．Virtualizeの有無による決定的な違い
### ３．１ Virtualizeなしの場合
- 当然すべての行を取得している
（50,000行のデータが、サーバー側とブラウザ上に展開される）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/fa7b4a11-887e-cef1-205b-fbb79f3d6f74.png)

- ブラウザのタブで3GB近くメモリを消費してしまう
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/5129f9c9-c4ff-b79b-a5a4-182638693613.png)
:::note alert
対象データ量に比例してメモリ消費が増える。
:::

### ３．２ Virtualizeありの場合
- 表示範囲＋α行しか取得しない
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/7913935f-c012-3e47-4a4e-cd4aebfe92d8.png)

- ブラウザのタブで20MB未満しか消費しない
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/e185a034-78d3-da0a-81f1-49f37b749231.png)
:::note info
3GB -> 20MB へ大幅に改善
:::

- スクロールすると取得しているエレメントの開始行がNo=1から変わる
（ブラウザで表示できる範囲＋αしか取得していない）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/83b3f8c2-a1a1-e10b-99f4-44a1571c293b.png)

- スクロールすると、データを取得するSignalR上のバイナリデータ通信が確認できる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/7f471a39-bbd9-a3c9-fa4e-02f22c7f36a4.png)


## ４．評価パターン
| No | パターン | サーバーメモリ | ブラウザメモリ | 応答時間 |
|:-:|:-|:-:|:-:|:-:|
|(1)| Virtualize なし  | 460MB | 2997MB | 7178ms |
|(2)| Virtualize 初期ロード  | 69MB  | 23MB | 1264ms |
|(3)| Virtualize 必要ページロード  | **3MB**  | **19MB** | **11ms** |

:::note 
圧倒的にVirtualize（特に必要ページをロード）が効率的なことがわかる
:::

### ４．１ Virtualizeの種類

| 種類 | Virtualize 初期ロード | Virtualize 必要ページロード|
|:-:|:-:|:-:|
| データ | すべてのデータを事前にロード | 必要な範囲のデータを動的に取得 |
| 指定方法 | Items 指定 | ItemsProvider 指定 |
| メモリ | 全データがメモリに常駐 | 表示範囲 + バッファのデータのみメモリに保持 |


### ４．２ 評価実行結果

### （１）Virtualize なし
- (82MB → 542MB) = 460MB
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/0012980e-a20d-5c1a-eaad-aa487a33c5b1.png)

### （２）Virtualize 初期ロード
- (82MB → 151MB) = 69MB
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/bdb09955-71ac-5552-edd8-ca1aee20cd82.png)


### （３）Virtualize 必要ページロード
- (82MB → 85MB) = 3MB
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/bd6f8a2b-2ffd-2d1d-fc56-25ca3a1ce8bb.png)

## ５．ソース

### （１）Virtualize なし
- OnInitializedAsyncで全データ取得
- 描画は普通のforeach

```html
<tbody>
    @foreach (var weather in weathers)
    {
        <tr>
            <td>@weather.No</td>
            <td>@weather.Date.ToShortDateString()</td>
            <td>@weather.TemperatureC</td>
            <td>@weather.Summary</td>
        </tr>
    }
</tbody>
```
```C#
@code {
    private List<Weather> weathers = new List<Weather>();

    protected override async Task OnInitializedAsync()
    {
        weathers = await WeatherService.GetAllWeathers();
    }
}
```
呼び出しているデータ取得
```C#
public async Task<List<Weather>> GetAllWeathers()
{
    var weathers = await _context.Weathers.ToListAsync();
    return weathers;
}
```



### （２）Virtualize 初期ロード
- OnInitializedAsyncで全データ取得
- 描画には ```<Virtualize>``` タグを利用
- ```Items="@weathers"``` にて全データを受け取る
```html
<tbody>
    <Virtualize Context="weather" Items="@weathers" OverscanCount="5" SpacerElement="tr">
        <tr>
            <td>@weather.No</td>
            <td>@weather.Date.ToShortDateString()</td>
            <td>@weather.TemperatureC</td>
            <td>@weather.Summary</td>
        </tr>
    </Virtualize>
</tbody>
```
```C#
@code {
    private List<Weather> weathers = new List<Weather>();

    protected override async Task OnInitializedAsync()
    {
        weathers = await WeatherService.GetAllWeathers();
    }
}
```


### （３）Virtualize 必要ページロード
- 描画には ```<Virtualize>``` タグを利用
- 描画用にデータが必要になった時、必要なデータだけを取得
（必要な件数のみ取得）
- ```ItemsProvider="LoadWeathers"``` にてデータが必要になった時に呼び出すイベントを指定

```html
<tbody>
    <Virtualize Context="weather" ItemsProvider="LoadWeathers" OverscanCount="5" SpacerElement="tr">
        <tr>
            <td>@weather.No</td>
            <td>@weather.Date.ToShortDateString()</td>
            <td>@weather.TemperatureC</td>
            <td>@weather.Summary</td>
        </tr>
    </Virtualize>
</tbody>
```
```C#
@code {
    private async ValueTask<ItemsProviderResult<Weather>> LoadWeathers(ItemsProviderRequest request)
    {
        var weathers = await WeatherService.GetRangeWeathers(request.StartIndex, request.Count);
        return new ItemsProviderResult<Weather>(weathers, totalCount);
    }
}
```
呼び出しているデータ取得
```C#
public async Task<List<Weather>> GetRangeWeathers(int startIndex, int Count)
{
    var weathers = await _context.Weathers
        .Skip(startIndex)
        .Take(Count)
        .OrderBy(x => x.No)
        .ToListAsync()
        ;

    return weathers;
}
```

## ６．おすすめの戦略


:::note 
データ量が少なく、シンプルさを求める場合
　　→ （２）Virtualize 初期ロード を利用
:::

:::note 
データ量が多く、効率的に取得したい場合
　　→ （３）Virtualize 必要ページロード を利用
:::


### （２）Virtualize 初期ロード が適しているケース
- データ量が少ない場合（例: 数百件程度）
- データがすでにメモリ上に存在しており、頻繁に変更されない場合
- 初期ロードのパフォーマンスを優先したい場合

### （３）Virtualize 必要ページロード が適しているケース
- データ量が多い場合（例: 数千〜数百万件）
- データを逐次取得することで、効率的にメモリとネットワークを活用したい場合
- リアルタイム更新や段階的なデータ取得が必要な場合

## ７．全ソース
https://github.com/masayahak/BlazorVR


## ８．参考
https://learn.microsoft.com/ja-jp/aspnet/core/blazor/components/virtualization?view=aspnetcore-9.0