shallowRefとcomputedWithControlでパフォーマンスチューニングしてみた

## この記事を書いた理由

Vueのref変数はネストした層までリアクティブに監視しています。
そのため柔軟な書き込みへの監視が可能ですが、大量のデータを扱う際にはパフォーマンスが低下しやすい問題があります。
そこで、今回はshallowRefとVueUseのcomputedWithControlという関数を使って変数をリアクティブに保ちつつパフォーマンス最適化を試してみました。

## shallowRefの概要

shallowRefとは、refのshallow(浅い)バージョンです。
ネストした書き込みを監視しないため、computedの再計算やwatch,再レンダリングは発火せず、再代入した場合のみ監視されます
これは大量のデータをもつオブジェクトや配列へリアクティブに書き込みたいが、都度再計算・即時反映させなくても良いケースで適しています。

https://ja.vuejs.org/api/reactivity-advanced#shallowref

参考：shallow~でのパフォーマンスチューニングについて

https://ja.vuejs.org/guide/best-practices/performance.html#reduce-reactivity-overhead-for-large-immutable-structures


### メリット
* 浅い監視のため複雑な構造・大量のデータでパフォーマンスを改善できる

### 注意点
* ネストした書き込み・変更は検知しないためリプレイスには要注意

## computedWithControlの概要

computedWithControlとは、依存関係を指定することで特定の変数への変更が加わった場合のみ再計算するcomputedです。
第１引数に再計算のトリガーとするリアクティブ変数を定義し、第2引数に計算式を書きます。

https://vueuse.org/shared/computedWithControl/

例：
```js
import {computedWithControl} from "@vueuse/core"

const sampleCount = ref(0)

const sampleComp = computedWithControl(()=>sampleCount,()=>{
    return sampleCount + "count!"
})

const onIncrement = () => {
    sampleCount.value++; //sampleCompの再計算が実行される
}
```

また、Vue3であれば以下のように手動で再計算を発火することができます。

```js
const sampleCount = ref(0)

//依存関係を敢えて定義しない
const sampleComp = computedWithControl(()=>[],()=>{
    return sampleCount + "count!"
})

const onResetCount = () => {
    sampleCount.value = 0;
    sampleComp.trigger() //ここでsampleCompが再計算され、値が反映される
}
```


### メリット
* 依存関係を絞り、手動での発火ができるため再計算のタイミングを制御しやすい

### デメリット
* ReactのuseEffectのような、複数変数を依存関係に定義することができない(もしご存知の方がいたら教えていただけますと助かります)

## どのように組み合わせたか

table構造でv-forを用い、セルにinputタグを1000件作成し、各自入力内容でAPIを実行。
その後fetchデータを各行へ注入する・・・というケースがありました。
inputにはそれぞれv-modelでバインドしていましたが入力中の監視は不要だったため平時はshallowRefで管理していました。

しかしながらfetchデータを注入するタイミングでObjectを再作成→再代入するのではオーバーヘッドが大きいと判断し、fetch後にcomputedWithControlの再計算を手動発火することでDOMへのリアクティブな反映もさせつつ最低限の計算量でデータ管理を実現できました。

イメージ

```html
<template>
    <!-- 再レンダリングされる -->
    <p>{{ currentRowResult }}</p>
    <table>
        <tr v-for="(el, idx) in largeData" :key="idx">
            <td>
                <input v-model="el.searchKey" @focus="currentIndex = idx" @blur="onRecomputed(el.searchKey, idx)" />
            </td>
            <td>
                <!-- ここは再レンダリングされない -->
                <p v-once>{{ el?.result ?? "empty" }}</p>
            </td>
        </tr>
    </table>
</template>
```

```js
<script setup lang="ts">
import { shallowRef, ref, onMounted, watch } from "vue"
import { computedWithControl } from "@vueuse/core"

type SampleItem = {
    searchKey: string,
    result: string
}

//フォーカスした行インデックスを格納
//※実装省略
const currentIndex = ref(0)

const currentRowResult = computedWithControl(() => [], () => {
    return largeData.value[currentIndex.value]?.result
})

const largeData = shallowRef<SampleItem[]>([]) //1000件以上のデータ

const onRecomputed = (seachKey: string, targetIndex: number) => {
    console.log(seachKey, targetIndex)
    //seachKeyでfetch

    //ダミーリザルト
    largeData.value[targetIndex].result = "Test-" + seachKey
}


onMounted(() => {
    //ダミーデータ
    for (let i = 0; i < 1000; i++) {
        largeData.value.push({ searchKey: "", result: "" })
    }
})
watch(currentIndex, () => {
    //再計算を手動で発火
    currentRowResult.trigger()
}, { flush: "post" })

</script>
```


computedWithControlをはじめとするVueUseはまだまだ情報が少ないですが、便利なUtilが多く提供されているので色々試していきたいと思います。