TOP メディア一覧 技術投稿│Vue3.5の主な新機能と改善点

2024.11.29

  • 技術情報
  • テックブログ

技術投稿│Vue3.5の主な新機能と改善点

サムネイル画像

自己紹介


ニックネーム:たろ
経験年数:2年
前職は鉄道マン。IT・Web業界に興味を持ち、1年間の独学を経て2022年1月に株式会社アクロクレインに中途入社。
Web系の開発をメインに日々奮闘中。


こんにちわ、たろです。

寒い季節になってきましたね。北海道旭川市では雪が降りはじめ、気温も一気に低くなりました…☃️

さて話は変わりますが、2024年9月にVueの公式ブログThe Vue Pointよりリリース名「天元突破グレンラガン」が発表され、Vue3.5は数々の新機能とパフォーマンス改善を引っ提げて登場しました。

本記事では、Vue3.5の主な新機能や改善点について、バージョン3.5以前と比較しながら見ていきたいと思います。

主な新機能


Reactive Props Destructure


Reactive Props Destructureとは、親コンポーネントから渡される値を分割代入で取り出した場合に、リアクティブ性を保持することができる機能のことです。

■使用例

<!-- ParentComponent.vue -->
<template>
  <h2>Reactive Props Destructure</h2>
  <input type="number" v-model="count" />
  <ChildComponent :count="count" />
</template>

<script setup lang="ts">
  import { ref } from "vue";
  import ChildComponent from "@/components/templates/ChildComponent.vue";

  const count = ref<number>(0);
</script>
<!-- ChildComponent.vue -->
<template>
  <p>Count up:{{ count }}</p>
  <p>Count up double:{{ double }}</p>
</template>

<script setup lang="ts">
  import { computed } from "vue";

  const { count } = defineProps<{ count: number }>();
  const double = computed(() => count * 2);
</script>

親コンポーネントから子コンポーネントへ props で渡した値を分割代入で count に代入し、その count の値が変更されたときに、count の2倍の値を double に代入する処理を行っています。

■表示内容

違いが分かりやすいようにバージョン3.5と3.4の表示内容を比較してみました。

Reactive Props Destructure表示内容
バージョン3.5

バージョン3.5では、input要素の入力値の変更に連動して、Count up doubleに2倍の数値が表示されており、countのリアクティブ性が失われていないことが確認できます。

Reactive Props Destructure表示内容
バージョン3.4

バージョン3.4では、input要素の入力値を変更してもCount upの数値は連動していますが、countのリアクティブ性が失われているため、countの変更が検知できず、Count up doubleの数値が0のままです。

■バージョン3.5以前の記述

<!-- ChildComponent.vue -->
<template>
  <p>Count up:{{ props.count }}</p>
  <p>Count up double:{{ double }}</p>
</template>

<script setup lang="ts">
  import { computed } from "vue";

  const props = withDefaults(
    defineProps<{
      count?: number;
    }>(),
    {
      count: 0,
    }
  );
  const double = computed(() => props.count * 2);
</script>

バージョン3.5以前でも withDefaults() を使用することで、分割代入した値のリアクティブ性を保持することができました。しかし、バージョン3.5以降の書き方では、withDefaults() を使用する必要がなくなったため、記述量が少なくなり、スッキリしました。

useTemplateRef()


バージョン3.5で、useTemplateRef()というtemplate内のDOM要素を参照する機能が追加されました。

useTemplateRef()は、参照したいテンプレート内のDOM要素にref属性を指定することで、そのDOM要素を取得することができます。

■使用例

<template>
  <p ref="elColorChange">The color changes!</p>
</template>

<script setup lang="ts">
  import { useTemplateRef, onMounted } from "vue";

  const elementRef = useTemplateRef('elColorChange');
  onMounted(() => {
    if (elementRef.value) {
      elementRef.value.style.color = 'lightgreen';
    }
  });
</script>

template内のp要素を参照するため、p要素にref属性を指定し、その属性値としてelColorChangeを入れています。

useTemplateRef()の引数にも同様にelColorChangeを指定することで、p要素を参照することができるようになります。

■バージョン3.5以前の記述

<template>
  <p ref="elementRef">The color changes!</p>
</template>

<script setup lang="ts">
  import { ref, onMounted } from "vue";

  const elementRef = ref<HTMLElement | null>(null);
  onMounted(() => {
    if (elementRef.value) {
      elementRef.value.style.color = "lightgreen";
    }
  });
</script>

バージョン3.5以前は、ref()を使用することでDOM要素を取得できていました。

ref属性に指定している属性値(elementRef)と同じ名前の変数を宣言し、初期値をnullにして初期化を行うという点がuseTemplateRef()との違いです。

Deferred Teleport


バージョン3.5からTeleportコンポーネントにdefer属性を指定することができるようになりました。defer属性が指定されたTeleportコンポーネントは、指定したDOM要素がレンダリングされるまでマウントされません。

■使用例

<template>
  <section id="sec01"></section>
  <Teleport to="#sec01">
    <div class="sec01">#sec01のコンテンツ</div>
  </Teleport>

  <Teleport defer to="#sec02">
    <div class="sec02">#sec02のコンテンツ</div>
  </Teleport>
  <section id="sec02"></section>
</template>

DOM要素がレンダリングされるまでマウントを待機させたいTeleportコンポーネントにdefer属性を指定します。

sec02のコンテンツは、id属性の属性値がsec02のDOM要素がレンダリングされるまでマウントを待機します。

レンダリングされたタイミングで、Teleportコンポーネントがマウントされるため、正常に表示されるようになります。

■defer属性を使用しない場合の挙動

defer属性を使用しない場合の挙動
警告文がログに表示され、sec02のコンテンツが表示されない…

バージョン3.5以前はdefer属性を使用することができなかったため、Teleportコンポーネントがマウントされた時点で指定したDOM要素が存在しない場合、そのDOM要素内にTeleportコンポーネントをマウントできず、表示させることができませんでした。

onWatcherCleanup()


onWatcherCleanup()は、現在のウォッチャーが再実行される直前に実行されるクリーンアップ関数を登録することができます。

■使用例

<template>
  <h2>onWatcherCleanup()</h2>
  <p>数値変更でカウントアップ開始</p>
  <input type="number" v-model="initialSeconds" min="0" />
  <p>タイム : {{ elapsedSeconds }}秒</p>
</template>

<script setup lang="ts">
  import { ref, watch, onWatcherCleanup } from 'vue'

  const initialSeconds = ref<number>(0)
  const elapsedSeconds = ref<number>(initialSeconds.value)

  watch(initialSeconds, newValue => {
    elapsedSeconds.value = newValue

    const intervalId: number = setInterval(() => {
      if (elapsedSeconds.value >= 0) {
        elapsedSeconds.value++
      } else {
        clearInterval(intervalId)
      }
    }, 1000)

    onWatcherCleanup(() => {
      clearInterval(intervalId)
    })
  })
</script>

inputの値を変更すると新しいカウントアップが実行されますが、実行するたびにonWatcherCleanup()内で以前のタイマーをクリアするように記述しています。

■表示結果

onWatcherCleanup使用例

onWatcherCleanup()により古いタイマーはクリーンアップされ、正常にカウントアップされていることが確認できます。

■onWatcherCleanup()を使用しない場合

onWatcherCleanupを使用しない場合

onWatcherCleanup() を使用せずに input の値を変更してみると、タイマーが異常な速さで加算されていきます。

原因は、initialSeconds が変更されるたびに watch が発火し、setInterval が新たに追加実行されるためです。その結果、同時に複数の setInterval が動作し、タイマーが予期せぬ速さでカウントアップしてしまいます。

主な改善内容

システムの最適化


■メモリ使用量の改善

バージョン3.5では、リアクティブシステムが大幅に改善され、メモリ使用量が従来と比べて56%も削減されています。
さらに、計算プロパティ(computed)において古い値が残ってしまう問題が解消されました。また、サーバーサイドレンダリング(SSR)時に、計算プロパティが原因でメモリリークが発生するケースも改善されています。

■リアクティブ配列に関する操作の高速化

大規模かつ深い構造を持つリアクティブな配列の操作においても大きな最適化が施され、複雑な配列操作が場合によっては従来よりも最大10倍高速に実行されるようになりました。
この改善は、Vueの動作に影響を与えることなく、より効率的で軽量な動作を可能にしています。

Lazy Hydration(遅延ハイドレーション)


Lazy Hydrationは、コンポーネントのハイドレーション※1の実行タイミングを制御することができる機能です。

ハイドレーションを遅延させたり、実行タイミングをずらすことで初期読み込み時間とリソース使用量を削減することができます。

それでは、導入されたハイドレート戦略を使用例※2と共に見ていきたいと思います。

※1 ハイドレーションとは、SSRを用いたページやアプリケーションにおいて、サーバーサイドで生成した静的なHTMLをクライアント側でJavaScriptを用いて動的でインタラクティブなコンテンツにするプロセスのことです。

※2 SSRの環境構築にはNuxt.jsを使用しています。


Hydrate on Idle(アイドル時にハイドレート)


ブラウザがアイドル状態であるタイミングでハイドレートが開始させることができます。

■使用例

<!-- App.vue -->
<template>
  <h1>アイドル時にハイドレート</h1>
  <AsyncHydrate />
</template>

<script setup lang="ts">
  import { defineAsyncComponent, hydrateOnIdle, onMounted } from "vue";

  /**
   * 意図的にアイドル状態を遅延する関数
   * @param {number} duration
   */
  const keepBusy = (duration: number) => {
    const end = Date.now() + duration;
    while (Date.now() < end);
    console.log("クライアント側での負荷処理完了");
  };

  onMounted(() => {
    keepBusy(5000);
  });

  const AsyncHydrate = defineAsyncComponent({
    loader: () => import("./Hydrate.vue"),
    hydrate: hydrateOnIdle(),
  });
</script>
<!-- Hydrate.vue -->
<template>
  <p>アイドル状態でハイドレート開始</p>
  <button @click="increment">カウント: {{ count }}</button>
</template>

<script setup lang="ts">
  import { ref } from "vue";

  const count = ref<number>(0);
  const increment = () => {
    count.value++;
  };
</script>

■表示内容

アイドル時にハイドレート

アイドル状態になるまではハイドレートが開始されないため、ボタンをクリックしてもカウントアップされない状態ですが、アイドル状態になるとハイドレートが開始され、カウントアップができるようになります。

Hydrate on Visible (表示時にハイドレート)


要素がビューポート内に入るタイミングでハイドレートを開始させることができます。

■使用例

<!-- App.vue -->
<template>
  <h1>表示時にハイドレート</h1>
  <p>スクロールして表示されるタイミングでハイドレート開始</p>
  <AsyncHydrate />
</template>

<script setup lang="ts">
  import { defineAsyncComponent, hydrateOnVisible } from "vue";

  const AsyncHydrate = defineAsyncComponent({
    loader: () => import("./Hydrate.vue"),
    hydrate: hydrateOnVisible(),
  });
</script>

<style scoped>
  p {
    margin-bottom: 500px;
  }
</style>
<!-- Hydrate.vue -->
<template>
  <div class="hydrate" :class="{ changeColor: isHydrated }">
    <p v-if="isHydrated">ハイドレーション完了</p>
  </div>
</template>

<script setup lang="ts">
  import { ref, onMounted } from "vue";

  const isHydrated = ref(false);
  onMounted(() => {
    isHydrated.value = true;
    console.log("ハイドレーション完了");
  });
</script>

<style scoped>
  .hydrate {
    height: 500px;
    background-color: #8ac8dd;
    display: flex;
    justify-content: center;
    align-items: center;
    text-align: center;
    transition: background-color 2s ease-in-out;
  }

  .hydrate.changeColor {
    background-color: #f089bd;
  }
</style>

■表示結果

表示時にハイドレート

初期表示時はHydrateコンポーネントがビューポート外にあるため、ハイドレートが開始されません。

スクロールしてHydrateコンポーネントがビューポート内に入ると、hydrateOnVisible()によってハイドレートが開始され、「ハイドレーション完了」の文字と背景色が変更されます。

Hydrate on Media Query (メディアクエリでハイドレート)


指定したメディアクエリに一致したタイミングでハイドレートを開始させることができます。

■使用例

<!-- App.vue -->
<template>
  <h1>メディアクエリでハイドレート</h1>
  <AsyncHydrate />
</template>

<script setup lang="ts">
  import { defineAsyncComponent, hydrateOnMediaQuery } from "vue";

  const AsyncHydrate = defineAsyncComponent({
    loader: () => import("./Hydrate.vue"),
    hydrate: hydrateOnMediaQuery("(max-width:375px)"),
  });
</script>
<!-- Hydrate.vue -->
<template>
  <p>画面幅を375px以下にするとハイドレート開始</p>
  <button @click="increment">カウント: {{ count }}</button>
</template>

<script setup lang="ts">
  import { ref } from "vue";

  const count = ref<number>(0);
  const increment = () => {
    count.value++;
  };
</script>

■表示結果

メディアクエリでハイドレート

画面幅が376px以上ではハイドレートが開始されないためカウントアップができませんが、画面幅が375px以下になるとハイドレートが開始され、Hydrateコンポーネントがインタラクティブとなるためカウントアップができるようになります。

Hydrate on Interaction (インタラクションでハイドレート)


コンポーネントに指定したイベントが実行されたタイミングでハイドレートを開始させることができます。

■使用例

<!-- App.vue -->
<template>
  <h1>インタラクションでハイドレート</h1>
  <p>クリックしたタイミングでハイドレートが開始</p>
  <AsyncHydrate />
</template>

<script setup lang="ts">
  import { defineAsyncComponent, hydrateOnInteraction } from "vue";

  const AsyncHydrate = defineAsyncComponent({
    loader: () => import("./Hydrate.vue"),
    hydrate: hydrateOnInteraction("click"),
  });
</script>
<!-- Hydrate.vue -->
<template>
  <div class="hydrate" :class="{ changeColor: isHydrated }">
    <p v-if="isHydrated">ハイドレーション完了</p>
  </div>
</template>

<script setup lang="ts">
  import { ref, onMounted } from "vue";

  const isHydrated = ref(false);
  onMounted(() => {
    isHydrated.value = true;
    console.log("ハイドレーション完了");
  });
</script>

<style scoped>
.hydrate {
  height: 300px;
  background-color: #8ac8dd;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  transition: background-color 2s ease-in-out;
}

.hydrate.changeColor {
  background-color: #f089bd;
}
</style>

■表示結果

インタラクションでハイドレート

Hydrateコンポーネント上でクリックすると、ハイドレートが開始されていることがわかります。

■複数のイベント指定

インタラクションでハイドレート(複数イベントの場合)

複数のイベントを指定することもできます。先程のclickイベントに加えてmouseoverイベントも追加しました。

hydrate: hydrateOnInteraction(["click", "mouseover"])

これにより、Hydrateコンポーネントをマウスホバーしてもハイドレートが開始されるようになります。

Custom Strategy (カスタムハイドレート戦略)


ハイドレート戦略を自分でカスタマイズし、作成することも可能です。

■使用例

<!-- App.vue -->
<template>
  <h2>Custom Strategy</h2>
  <p>Hydrateコンポーネントが読み込まれてから、一定時間後にハイドレート開始</p>
  <AsyncHydrate />
</template>

<script setup lang="ts">
  import { defineAsyncComponent, type HydrationStrategy } from "vue";

  const myCustomStrategy: HydrationStrategy = (hydrate) => {
    const timerId = setTimeout(() => {
      hydrate();
      clearTimeout(timerId);
    }, 3000);
    return () => clearTimeout(timerId);
  };

  const AsyncHydrate = defineAsyncComponent({
    loader: () => import("./Hydrate.vue"),
    hydrate: myCustomStrategy,
  });
</script>
<!-- Hydrate.vue -->
<template>
  <div class="hydrate" :class="{ changeColor: isHydrated }">
    <p v-if="isHydrated">ハイドレーション完了</p>
  </div>
</template>

<script setup lang="ts">
  import { ref, onMounted } from "vue";

  const isHydrated = ref(false);
  onMounted(() => {
    isHydrated.value = true;
    console.log("ハイドレーション完了");
  });
</script>

<style scoped>
.hydrate {
  height: 300px;
  background-color: #8ac8dd;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  transition: background-color 2s ease-in-out;
}

.hydrate.changeColor {
  background-color: #f089bd;
}
</style>

■表示結果

Custom Strategy
一定時間後にハイドレートを開始するmyCustomStrategy()を作成しています。

useId


useId() を使うことで、クライアントとサーバーの両方で生成されたIDが一致するように、一意のIDを生成することができます。

■使用例

<template>
  <h2>useId()</h2>
  <div>
    <label :for="id1">label name01 </label>
    <input :id="id1" />
  </div>
  <div>
    <label :for="id2">label name02 </label>
    <input :id="id2" />
  </div>
</template>

<script setup lang="ts">
  import { useId } from 'vue'

  const id1:string = useId()
  const id2:string = useId()
</script>

■表示結果

useId使用例

検証ツールで生成されたIDを確認してみるとv-0、v-1のようなユニークなIDが生成されます。

■app.config.idPrefix

app.config.idPrefix使用例

またmain.tsにapp.config.idPrefixを追記することで、IDにプレフィックスを付けることができます。

app.config.idPrefix = 'my-prefix'

data-allow-mismatch


data-allow-mismatchは、サーバーサイドレンダリング(SSR) 環境でのハイドレーションにおいて、不一致を許可するために使用される属性です。

■使用例

<template>
  <div data-allow-mismatch>
    <p>{{ message }}</p>
  </div>
</template>

<script setup lang="ts">
  import { ref } from "vue";

  const message = ref("Hello, World!");
</script>

この属性が設定されたコンポーネントは、サーバーで生成されたHTMLとクライアントでのレンダリング結果が一致しない場合でも、ハイドレーションプロセスが続行されます。

※ data-allow-mismatch属性を使用することでハイドレーションの不一致を許可することはできますが、不一致により意図しない動作やバグが発生する可能性があるため、使用する際は注意が必要です。

カスタム要素の改善


バージョン3.5では、defineCustomElement API に関連する問題が改善され、以下の機能が追加されました。

■configureApp

カスタム要素内でVueアプリケーション設定を行うことができます。

■useHost()

カスタム要素がマウントされているホスト要素※1を参照することができます。例えばホスト要素のプロパティや状態を参照したり、変更を加えることができます。

■useShadowRoot()

カスタム要素内でそのシャドウDOM※2を操作するために使われます。シャドウDOMを参照することで、スタイルの動的な変更やシャドウDOM内部の要素の操作が可能になります。

■shadowRoot: false

カスタム要素はシャドウDOMを使用せず、通常のDOMとして扱われるようになります。そのためshadowRoot: falseに設定すると、カスタム要素内のスタイルや構造は、外部のスタイルやスクリプトから影響を受けるようになります。

■nonce

カスタム要素に挿入されるstyle要素にnonce属性を追加できるようになりました。nonceは、style要素に追加して、コンテンツ・セキュリティ・ポリシー(CSP)※3で許可されたスタイルを適用させるために使用され、style要素のnonce属性に一意のトークン(ランダムな文字列)を設定できます。

※1 ホスト要素とは、カスタム要素が埋め込まれる親要素のこと

※2 シャドウDOM(Shadow DOM)とは、Webコンポーネントの内部構造を他の部分から隠すための仕組み

※3 コンテンツ・セキュリティ・ポリシー(CSP)とは、Webサイトが外部の攻撃から守るために使用するセキュリティ機能の1つ

まとめ


ここまでお読みいただき、ありがとうございました。

今回は、Vueのバージョン3.5について紹介させていただきました。内容を確認している中で、これまで知らなかった機能についても学ぶ良い機会となり、個人的にこのテーマで執筆できてよかったと思っています。

本記事が、少しでもVue 3.5についての理解を深める手助けとなれば幸いです!

この記事をシェアする

  • X
  • facebook