🚄

【Vue3】v-memoディレクティブでレンダリングを最適化する

2021/09/04に公開

Vue3.2からv-memoディレクティブが追加されました。
v-memoディレクティブを利用することで、不要な再レンダリングを省略することができるので、パフォーマンスの向上につながります。

概要

v-memoディレクティブは、指定したすべての値が最後のレンダリング結果と同じであれば、サブツリー全体の再レンダリングをスキップします。

v-memoディレクティブの書き方

以下のように、コンポーネントが再レンダリングされた際に、配列に指定したvalueA及びvalueBが、最後のレンダリング時と同じ値の場合は、div以下の要素の再レンダリングがスキップされます。

v-memoの書き方
<div v-memo="[valueA, valueB]">
  値に変更がなければ、この中身の再レンダリングがスキップされる
</div>
サンプルコード

ChildA.vueとChildB.vueを作成し、それぞれonUpdated()内にログを出力することで、再レンダリングされたことを確認できるようにします。

また、後ほど再レンダリングをするためにpropsmsgを受け取ります。

/src/components/ChildA.vue
<template>
  <div class="child-a">ChildA: {{ msg }}</div>
</template>

<script>
import { onUpdated } from "vue";
export default {
  name: "ChildA",
  props: {
    msg: {
      type: String,
      required: true,
    },
  },
  setup() {
    onUpdated(() => {
      console.log("ChildA.vueが再レンダリングされました");
    });
  },
};
</script>
/src/components/ChildB.vue
<template>
  <div class="child-b">ChildB: {{ msg }}</div>
</template>

<script>
import { onUpdated } from "vue";
export default {
  name: "ChildB",
  props: {
    msg: {
      type: String,
      required: true,
    },
  },
  setup() {
    onUpdated(() => {
      console.log("ChildB.vueが再レンダリングされました");
    });
  },
};
</script>

そして、App.vueで上記2つのコンポーネントをインポートし、ChildA.vueは通常通りpropsmsgを渡します。

また、ChildB.vueはpropsmsgを渡しつつ、v-memodummy変数を指定します。

/src/App.vue
<template>
  <child-a :msg="msg" />
  <child-b v-memo="[dummy]" :msg="msg" />
  <p>再レンダリング用: {{ dummy }}</p>

  <button @click="clearMsg">メッセージ初期化</button>
  <button @click="countUp">カウントアップ</button>
</template>

<script>
import { ref } from "vue";
import ChildA from "@/components/ChildA.vue";
import ChildB from "@/components/ChildB.vue";

export default {
  name: "App",
  components: {
    ChildA,
    ChildB,
  },
  setup() {
    const msg = ref("メッセージ");
    const clearMsg = () => {
      msg.value = "";
    };
    // 再レンダリング用
    const dummy = ref(0);
    const countUp = () => {
      dummy.value++;
    };

    return {
      msg,
      clearMsg,
      dummy,
      countUp,
    };
  },
};
</script>

こうすることで、通常であればmsgが更新されると、propsmsgを受け取っている2つのコンポーネントは再レンダリングされますが、ChildB.vueはv-memoの指定により、dummyが変更された場合のみ再レンダリングされるため、「メッセージを初期化」ボタンをクリックしても再レンダリングされません。

逆に、「カウントアップ」ボタンをクリックすると、ChildB.vueが再レンダリングされます。

また、v-memoの値に指定する配列を空にすると、v-onceと同等の機能になります。

v-memoの書き方
<div v-memo="[]">
  v-once(初回レンダリング以降は再レンダリングされない)と同じ
</div>
サンプルコード

以下コードは、カウントアップボタンを押してもv-memo="[]"の方は画面が更新されません。

<template>
  <p>通常: {{ value }}</p>
  <p v-memo="[]">v-memo="[]": {{ value }}</p>

  <button @click="incVal('value')">カウントアップ</button>
</template>

<script>
import { ref } from "vue";

export default {
  name: "App",
  setup() {
    const value = ref(0);

    const incVal = () => {
      value.value++;
    };

    return {
      value,
      incVal,
    };
  },
};
</script>

v-forとの併用

v-memoの最も一般的なケースであるv-forv-memoの併用方法です。

10個のリストを表示し、ボタンをクリックすると選択中のリストが変更されるサンプルコードです。

<template>
  <button @click="reselectItemId">選択中のIDを変更</button>
  <ul
    v-for="itemId in itemIdList"
    :key="itemId"
    v-memo="[itemId === selectedItemId]"
  >
    <li>
      ID: {{ itemId }}, selected:
      {{ itemId === selectedItemId ? "← 選択中" : "" }}
    </li>
  </ul>
</template>

<script>
import { reactive, ref } from "vue";

export default {
  name: "App",
  setup() {
    const maxItem = 10;
    const itemIdList = reactive([]);
    for (let i = 0; i < maxItem; i++) {
      itemIdList.push(i);
    }

    const selectedItemId = ref(0);

    const reselectItemId = () => {
      const rand = Math.floor(Math.random(maxItem) * 10);
      selectedItemId.value = rand;
    };

    return {
      itemIdList,
      selectedItemId,
      reselectItemId,
    };
  },
};
</script>

上記コードは、「選択中のIDを変更」ボタンをクリックすると、先程選択中だったリストと、新たに選択されたリストの2つのみが更新され、残りの要素は更新されません。

今回は例のため10個のリストですが、これが1000個や10,000個のリストなど、数が大きくなった場合はパフォーマンスの向上が見込めます。

参考

https://v3.vuejs.org/api/directives.html#v-memo

Discussion