こんにちは、かつコーチです。
前回は、リアクティビティが効かない現象の原因と対処法を掘り下げました。
今回は視点を変えて、「動くけど遅い」Vueアプリをどう改善していくか、パフォーマンスチューニングの入門編を扱います。
やみくもに最適化するのではなく、どこがボトルネックなのかを計測してから手を打つという順番を大切にしていきます。
パフォーマンス改善の基本方針
「体感で遅い」を「数字で遅い」に変える
パフォーマンスチューニングで一番やってはいけないのが、「なんとなく遅い気がする」という感覚だけで闇雲にコードを書き換えることです。
まずはブラウザの開発者ツールの「Performance」タブや、Vue Devtoolsの「Performance」機能で、実際にどの処理に時間がかかっているかを計測しましょう。
私も以前、「このコンポーネントが重そうだから」という思い込みだけで最適化を進めて、実際にはまったく別のコンポーネントの再レンダリングがボトルネックだった、ということがありました。
計測せずに直感だけで着手すると、労力のわりに効果が出ない改修になりがちです。
チューニングの優先順位
Vueアプリのパフォーマンス問題は、大きく次の3種類に分けられます。
- 不要な再レンダリング:本来更新しなくていい部分まで再描画されている
- 初期表示の遅さ:バンドルサイズが大きく、最初の読み込みに時間がかかる
- 大量データの描画の重さ:一覧表示などでDOMの数が多すぎる
まずはユーザーの体感に直結しやすい「1. 不要な再レンダリング」と「3. 大量データの描画」から着手し、その後に「2. 初期表示」のバンドル最適化に取り組む、という順番がおすすめです。
不要な再レンダリングを防ぐ
v-onceで一度きりの描画にする
一度描画したら二度と変わらない部分(静的な見出しや説明文など)には、v-onceを使うと、以降の再レンダリング対象から外すことができます。
<script setup lang="ts">
import { ref } from 'vue'
const count = ref<number>(0)
</script>
<template>
<!-- countが変わっても、この部分は一度描画されたら更新されない -->
<h1 v-once>アプリの使い方ガイド</h1>
<p>カウント: {{ count }}</p>
<button @click="count++">+1</button>
</template>
v-onceは「初回描画時の内容で確定し、以降は一切更新しない」という指定なので、動的に変化する部分には使えません。
あくまで「変化しないと分かっている静的な部分」限定のテクニックです。
computedのキャッシュを活かす
Vueのcomputedは、依存する値が変わらない限り再計算されずにキャッシュされた値を返してくれます。
❌ Before:テンプレート内で毎回同じ処理を関数呼び出しする
<script setup lang="ts">
import { ref } from 'vue'
interface Item {
price: number
quantity: number
}
const items = ref<Item[]>([
{ price: 100, quantity: 2 },
{ price: 300, quantity: 1 },
])
// この関数はテンプレートが再描画されるたびに毎回実行される
const calcTotal = (): number => {
return items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
</script>
<template>
<p>合計金額: {{ calcTotal() }}円</p>
</template>
メソッドとしてテンプレートから呼び出す関数は、そのコンポーネントが再レンダリングされるたびに毎回実行されます。
計算量が小さいうちは問題になりませんが、重い計算処理をテンプレート内で何度も呼び出す設計は、規模が大きくなるほどボトルネックになりやすいです。
✅ After:computedにして依存する値が変わったときだけ再計算する
<script setup lang="ts">
import { ref, computed } from 'vue'
interface Item {
price: number
quantity: number
}
const items = ref<Item[]>([
{ price: 100, quantity: 2 },
{ price: 300, quantity: 1 },
])
// itemsが変化したときだけ再計算され、それ以外は前回の計算結果を使い回す
const total = computed<number>(() => {
return items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
})
</script>
<template>
<p>合計金額: {{ total }}円</p>
</template>
「テンプレートで参照するたびに再計算が必要かどうか」を意識し、依存元が変わらない限り同じ結果になる処理はcomputedに寄せるのが基本方針です。
大量データの描画を軽くする
v-showとv-ifの使い分け
表示・非表示を頻繁に切り替える要素には、v-ifよりv-showが適しています。
v-ifは切り替えのたびにDOM要素の生成・破棄が発生しますが、v-showはdisplay: noneの切り替えだけで済むため、頻繁な切り替えではコストを抑えられます。
<template>
<!-- 頻繁に開閉するタブの中身は、生成・破棄のコストが低いv-showが向いている -->
<div v-show="activeTab === 'settings'">設定画面の中身</div>
<!-- ログイン状態のように、一度切り替わったら長く変わらない場合はv-ifで問題ない -->
<div v-if="isLoggedIn">ようこそ</div>
</template>
逆に、そもそも表示される可能性が低い要素(権限がない場合の管理者メニューなど)は、初期描画のコストを避けるためv-ifが向いています。
「頻繁に切り替わるならv-show、めったに切り替わらないならv-if」と覚えておくと迷いません。
仮想スクロールで大量の一覧を軽くする
数千件を超えるような一覧をv-forでそのまま描画すると、DOM要素の数が膨大になり、スクロールがカクついてしまいます。
このようなケースでは、画面に見えている範囲のDOMだけを描画する仮想スクロール(Virtual Scroll)というテクニックが有効です。
<script setup lang="ts">
import { ref } from 'vue'
interface Product {
id: number
name: string
}
// 5000件の商品データを想定
const products = ref<Product[]>(
Array.from({ length: 5000 }, (_, i) => ({ id: i, name: `商品${i}` }))
)
</script>
<template>
<!-- vue-virtual-scrollerのようなライブラリを使うと、画面内の分だけをDOM化できる -->
<RecycleScroller
class="scroller"
:items="products"
:item-size="40"
key-field="id"
v-slot="{ item }"
>
<div class="item">{{ item.name }}</div>
</RecycleScroller>
</template>
自前で仮想スクロールを実装するのは骨が折れるため、vue-virtual-scrollerのようなライブラリを使うのが現実的です。
すべての一覧に必要な技術ではありませんが、「一覧の件数が増えるとスクロールが重くなる」と感じたら、まずこの手法を検討してみてください。
初期表示を速くする
非同期コンポーネントによる遅延読み込み
最初の画面表示に不要なコンポーネント(モーダルやタブの中身など)は、defineAsyncComponentで必要になったタイミングまで読み込みを遅らせられます。
<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue'
// モーダルの中身は、開くまでバンドルに含めなくてよい
const SettingsModal = defineAsyncComponent(() => import('./SettingsModal.vue'))
const isModalOpen = ref<boolean>(false)
</script>
<template>
<button @click="isModalOpen = true">設定を開く</button>
<SettingsModal v-if="isModalOpen" />
</template>
defineAsyncComponentで読み込みを遅延させると、初期表示に必要なJavaScriptの量が減り、初回のページ表示が速くなります。
Vue Routerを使ったルーティングでも、ページ単位でこの手法を使うのが定番です。
// router/index.ts
const routes = [
{
path: '/settings',
// ルートに入るまで、このページ用のコードを読み込まない
component: () => import('../views/SettingsView.vue'),
},
]
「最初の画面表示に本当に必要なものだけを最初に読み込む」という発想が、初期表示速度改善の基本です。
つまずきやすいポイント:最適化しすぎて可読性が落ちる
かつコーチが実際につまずいた話
パフォーマンスチューニングを学び始めた頃、私は「とにかくcomputedとv-onceを使いまくれば速くなるはずだ」と考えて、静的にしか見えない部分にまで手当たり次第v-onceを付けて回ったことがあります。
結果、後になって「実は将来的に動的にしたい」という要件が入ったときに、v-onceが原因で更新されない不具合を生み、原因調査に時間を取られました。
計測して初めてボトルネックだと分かった箇所だけを最適化する、というのが遠回りに見えて実は一番効率的でした。
最適化前に計測、最適化後にも計測
パフォーマンス改善は、Before/Afterで数値を比較して初めて効果があったと言えます。
「なんとなく速くなった気がする」で終わらせず、改善前後で開発者ツールの計測結果を比較する習慣をつけましょう。
計測なしの最適化は、コードの複雑さだけを増やして効果がゼロ、というケースも珍しくありません。
まとめ
この記事のポイント
- パフォーマンス改善は感覚ではなく、開発者ツールでの計測から始める
- 静的な部分には
v-once、重い計算処理はcomputedでキャッシュを活かす - 頻繁な表示切り替えは
v-show、大量データの一覧は仮想スクロールを検討する - モーダルやページ単位のコンポーネントは
defineAsyncComponentで遅延読み込みする - 最適化は計測してボトルネックと分かった箇所だけに絞り、改善前後で必ず数値を比較する
次に読むべき記事
次回は、Vueプロジェクトのディレクトリ構成のベストプラクティスについて解説します。
→ 次の記事:Vueプロジェクトのディレクトリ構成のベストプラクティス