Vue 2 → Vue 3 + Vuetify 3 移行計画の全貌 ― 段階的アップグレードの戦略と実践

Vue 2 から Vue 3 + Vuetify 3 への移行計画

はじめに

Miyagi です。今回、実際のプロジェクトで Vue 2 → Vue 3 の移行に取り組みました。Vue 2 は 2023年12月31日に EOL を迎えており、セキュリティアップデートやエコシステムの最新化を考えると、もはや移行は避けて通れない状況です。移行期間中にリリースされた Vite 8(Rolldown ベース)も取り込んだ結果、ビルド時間は約 7 倍、開発サーバー起動は約 25 倍高速化しました。工数は約 1.5 人月です。

対象は Vue 2.6 + Vuetify 2 + Vuex + vee-validate 3 + Vue CLI (Webpack 4) という、2020年頃の「鉄板構成」で作られたプロジェクトです。.vue ファイルは約 70 個、Vuex モジュールが約 10 個、バリデーション付きフォームが約 20 画面。規模としては中くらいですが、関連ライブラリが軒並みメジャーバージョンアップを要求してくるので、どこから手をつけるかの計画が肝でした。

本記事では以下について説明します:

 

概要 ― 何を・どう変えたか

先に結論を書いておくと、「Vue 2.7 を中間ステップに使って段階的に移行する」という戦略が正解でした。 工数としては約 1.5 人月で、計画策定から単体テストまでを完了しています。 一発で Vue 3 に飛ぶのではなく、まず Vite に乗せ換えてから Vue 3 に上げるという二段構えです。

技術スタックの変更をざっくりまとめるとこんな感じです。

項目 移行前 移行後
フレームワーク Vue 2.6 Vue 3.5
UI ライブラリ Vuetify 2.6 Vuetify 3.12
状態管理 Vuex 3 Pinia 3
バリデーション vee-validate 3 vee-validate 4
ビルドツール Vue CLI 4 (Webpack 4) Vite 8
ルーティング Vue Router 3 Vue Router 4
Node.js 14.16.1 22.x LTS

見ての通り、ほぼ全部変わります。Vue 本体だけの話ではなく、周辺ライブラリが全て破壊的変更を含むメジャーアップデートなので、計画なしに突撃すると確実に詰みます。

コードベースの規模感としては、.vue ファイル約 70 個、Vuex モジュール約 10 個、バリデーション付きフォーム約 20 画面、ルート定義 30 以上。大規模プロジェクト(100画面以上)と比べれば小さいですが、それでも移行対象としては十分なボリュームです。なお、AI ツールの活用により移行作業のコスト自体は以前と比べて大幅に下がっています。ただし Dev 環境・Stg 環境での結合テストはまた別の話であり、そこは依然として課題です。本記事ではバージョンアップの考え方と単体テストまでを扱います。

 

設計判断 ― @vue/compat を使わなかった理由

Vue 3 移行といえば @vue/compat(Migration Build)が公式から提供されています。「互換モードで動かしながら段階的に修正できる」という触れ込みで、一見すると楽そうに見えます。

ただ、調べてみると Vuetify を使っている場合は実質的に使えません。

組み合わせ 動作 理由
@vue/compat + Vuetify 2 不可 Vuetify 2 が Vue 2 の VNode プライベート API に依存しており、compat でカバーされない
@vue/compat + Vuetify 3 不可 Vuetify 3 は Vue 3 ネイティブの Composition API が前提。互換モードの Vue 2 動作と矛盾する

Vue 公式の Migration Guide にも「Vuetify, Quasar, ElementUI 等に依存するプロジェクトは Vue 3 対応版を待つべき」と書いてありますし、コミュニティでも @vue/compat + Vuetify の成功事例はほぼ見つかりませんでした。

ということで、代わりに Vue 2.7 を中間ステップとして使う戦略を採りました。

  • Vue 2.7 + Vuetify 2 → 動作実績あり
  • Vue 2.7 + Vite → 動作実績あり(@vitejs/plugin-vue2 を使用)
  • Vue 2.7 → Vue 3 は差分が明確で対応しやすい

つまり、「まず Webpack を捨てて Vite に乗り換え、それから Vue 3 に上げる」という順番です。この順番にしたのは、Webpack 4 が Node.js 22 の OpenSSL 3 と互換性がなく、先に Vite に移行しないと Node.js を上げられないという事情もありました。

 

移行フェーズの全体像

計画は 8 つのフェーズに分けました。各フェーズの完了条件は「開発サーバーがエラーなく起動し、全画面が表示できること」。途中で起動不能になる期間をできるだけ短くするのが方針です。

Phase A: Vue 2.7 + Vite 移行(+ Node.js 22)
    │     ★ 起動可能(Vue 2.7 + Vuetify 2 + Vite)
    ▼
Phase B: Vue 3 + Vue Router 4 + Vuetify 3
    │     ★ 起動可能(見た目の差異は後で直す)
    ├──────────┐
    ▼          ▼
Phase C:   Phase D:
Pinia      vee-validate 4
    │          │
    └──────┬───┘
           ▼
Phase D2: ワーニング解消
    ▼
Phase D3: ライブラリアップデート
    ▼
Phase E: Vuetify 3 見た目のデグレ修正
    ▼
Phase F: テストケース作成
    ▼
Phase G: クリーンアップ・最終検証

Phase C(Pinia 移行)と Phase D(vee-validate 4 移行)は Vue 3 さえ動いていれば独立して進められるので、並行作業も可能です。

また、もう一つ重要な判断として、Options API を維持することにしました。Vue 3 でも Options API は公式サポートされていますし、約 70 ファイル全てを Composition API に書き換える工数は移行の本質ではありません。あくまでバージョンアップの完遂にフォーカスし、不必要なリファクタリングはスコープに含めない判断です。

 

各フェーズの詳細と所感

Phase A: Vue 2.7 + Vite 移行

最初のフェーズが一番重要でした。ここで失敗すると全体計画が崩れるので慎重に進めました。

やったことは大きく分けて3つ。Vue 2.6 → 2.7 へのアップグレード、Vue CLI (Webpack) → Vite への移行、Node.js 14 → 22 へのアップグレードです。

環境変数の移行が地味に面倒でした。Vue CLI では VUE_APP_* + process.env.VUE_APP_* だったのが、Vite では VITE_* + import.meta.env.VITE_* に全て変わります。grep で一括置換しましたが、.env ファイルのテンプレートやドキュメントの更新も含めると漏れが出やすいポイントです。

あと、require()require.context を使っている箇所は ESM の import / import.meta.glob に書き換える必要があります。Vue CLI 時代の vue.config.js も丸ごと不要になるので、vite.config.js を新規作成しました。

不要になったパッケージも多く、@vue/cli-servicesass-loadercore-jsbabel-eslint あたりは全部削除。Vite が Sass をネイティブサポートしているので、ローダー系は基本不要になります。

 

Phase B: Vue 3 + Vue Router 4 + Vuetify 3(起動可能にする)

ここが移行作業の山場です。Vue 3、Vue Router 4、Vuetify 3 の 3 つを同時に移行する必要がありました。なぜかというと、Vuetify 2 は Vue 3 で動かず、Vuetify 3 は Vue 2 で動かないので、中間状態が存在しないからです。

結果的に約 80 ファイルの変更になりました。主な作業は以下の通り。

Vue 3 本体: new Vue()createApp()Vue.use()app.use() への変更。beforeDestroybeforeUnmount のライフサイクル名変更、$router.push のコールバック廃止への対応なども。

Vue Router 4: new VueRouter()createRouter() + createWebHistory() に書き換え。mode: 'history' のようなオプション形式が関数形式に変わります。catch-all ルートも path: '/*'path: '/:pathMatch(.*)*' と変更が必要でした。

Vuetify 3: これが最も工数がかかりました。prop 名の変更が大量にあります。

  • depressedvariant="flat"(約 30 箇所)
  • outlinedvariant="outlined"(約 100 箇所)
  • densedensity="compact"(約 60 箇所)
  • item-textitem-title(約 10 箇所)
  • v-slot:activator="{ on, attrs }"v-slot:activator="{ props }" に変更(約 20 箇所)

v-data-table は API が大幅に変わっていて、サーバーサイドページネーション用の v-data-table-server への移行が必要でした。v-model 周りも、@input@update:model-value への変更が全フォーム要素に及びます。

このフェーズのポイントは 「見た目の完璧さは後回しにする」 こと。まずコンパイルエラーを解消して起動できる状態にすることに集中しました。デグレ修正は Phase E に回すことで、Phase B の作業が膨らみすぎるのを防ぎました。

なお、Vuex は一旦 Vuex 4 に上げるだけにとどめ(Vue 3 で動かすため)、Pinia への本格移行は次のフェーズに回しました。vee-validate も同様で、スタブ化して「バリデーションなしだが起動はする」状態を作りました。

 

Phase C: Vuex → Pinia 移行

Pinia は Vue 公式推奨の状態管理ライブラリで、事実上の Vuex 5 です。Options API スタイルの defineStore で定義できるので、既存のコードスタイルとも相性が良いです。

主な変更点はこんな感じ。

Vuex Pinia
new Vuex.Store({ modules: {...} }) defineStore() で個別定義
mutations + actions actions のみ(mutations 不要に)
state(オブジェクト) state(関数で返す)
Store.dispatch('Module/action') const store = useStore(); store.action()
namespaced modules 独立した store

約 10 個の Vuex モジュールを全て Pinia store に変換し、コンポーネント側の呼び出しも約 30 ファイルで書き換えました。mutations がなくなる分、コード量は減ります。約 50 ファイル変更で、ほぼ等量の書き換えでした。

コードレビューで指摘があったのは、action と getter の名前衝突。Vuex では namespace で区切られていたので同名でも問題なかったのですが、Pinia では 1 つのオブジェクト内に共存するので衝突します。命名を見直す必要がありました。

 

Phase D: vee-validate 4 移行

個人的に一番大変だったフェーズです。vee-validate は 3 → 4 で API が完全に刷新されていて、段階的な移行パスがありません。

vee-validate 3 vee-validate 4
<ValidationProvider> <Field> コンポーネント
<ValidationObserver> <Form> コンポーネント
extend('ruleName', {...}) defineRule()

約 20 ファイルのフォームを全て書き直しました。Options API を維持する方針だったので、Composition API ベースの useForm / useField ではなく、<Form> / <Field> コンポーネント方式を採用しています。

コードレビューで見つかったバグとしては、handleSubmit(submitForm()) のように関数を即時呼び出ししてしまっていたもの(正しくは handleSubmit(submitForm) と関数参照を渡す)がありました。vee-validate 4 の handleSubmit はコールバックにフォームフィールド値を渡してくるので、既存ロジックとの兼ね合いも確認が必要です。

 

Phase D2〜D3: ワーニング解消・ライブラリアップデート

Phase E のデグレ比較の前に、ビルド・lint のワーニングをゼロにするフェーズを挟みました。ワーニング修正でコンポーネントの描画が変わる可能性があるため、クリーンな状態でデグレ比較したかったからです。

::v-deep:deep() の置換(scoped CSS の場合)や、非 scoped の .scss ファイルでは ::v-deep を単純に除去して通常セレクタに変換するなど、地味ですが確実に潰しました。

ライブラリアップデートでは、axios 0.24 → 1.9、date-fns 2 → 4、Vite 6 → 7 などのメジャーアップデートも実施。qs パッケージは axios 1.x の組み込み paramsSerializer で代替できたので削除しました。

// 変更前
import qs from "qs";
paramsSerializer: (params) => {
  return qs.stringify(params, { arrayFormat: "repeat" });
},

// 変更後(axios 1.x)
paramsSerializer: { indexes: null },
Phase E: Vuetify 3 見た目のデグレ修正

Phase B で「起動できればOK」としていた見た目の差異を、ここでまとめて修正しました。Vue 2 版の画面と並べて表示しながら、カラー・スペーシング・レイアウトの差異を一つずつ潰していく作業です。

Vuetify 3 は CSS 変数ベースのテーマに変わっているので、Vuetify 2 時代の Stylus/SASS 変数を使ったカスタマイズは全て書き直しです。SCSS の @import@use / @forward への移行もこのフェーズで対応しました。

v-data-tablev-dialogv-menuv-date-picker あたりは表示位置やサイズ感がかなり変わっていて、調整に時間がかかりました。

Vuetify 3 の修正をする際に学んだ教訓が一つあります。 実装に入る前に必ず Vuetify 3 の公式ドキュメント(API リファレンス)を確認すること。Vuetify 2 時代の prop 名やクラス名をそのまま使って「動かない」と悩む時間が無駄だったことが何度かありました。background-colorbg-color のように、似ているけど違う名前に変わっているものが多いです。

 

Phase F: テストケース作成

移行の品質を担保するために、ユニットテスト(Vitest)と E2E テスト(Playwright)を整備しました。

  • Vitest ユニットテスト: 約 240 ケース(API 通信・ストア・バリデーション・ユーティリティ)
  • Playwright E2E: 約 160 ケース(全画面表示・画面遷移・パターン網羅)

Vue 2 のテストは実施しませんでした。API が非互換なので、Vue 2 版のテストコードは Vue 3 では動きません。代わりに Phase E で視覚的な比較を済ませてあるので、Vue 3 側でのテストに集中しました。

 

Phase G: クリーンアップ・最終検証

最終フェーズでは、ビルドチャンク分割、ESLint 8 → 10(flat config 移行)、Prettier 2 → 3、Vite 7 → 8 のアップデートなどを実施。

ビルドチャンク分割では、Vuetify を独立チャンク(534 kB)に、Vue / Vue Router / Pinia を vendor チャンク(107 kB)に分離。メインの index.js が 1,062 kB → 418 kB に削減されました(-60%)。

パフォーマンス比較の結果も記録しておきます。

項目 Vue 2 (Webpack 4) Vue 3 (Vite 8) 改善率
本番ビルド時間 18.40 秒 2.67 秒 約 7 倍高速
開発サーバー起動時間 6,154 ms 241 ms 約 25 倍高速

開発サーバーの起動が 25 倍速くなったのは体感でもはっきり分かるレベルで、これだけでも移行した甲斐がありました。

 

移行期間中の Vite 8 リリースと対応

移行作業中に Vite 8 がリリースされました。Vite 7 で進めていた最中だったので一瞬焦りましたが、調査したところ設定ファイルの変更は不要で、そのまま取り込むことができました。

Vite 8 は Rolldown(Rust ベースのバンドラ)を採用しています。Vite 7 との比較では、ビルド・起動ともに動作やテスト結果に差異はなく、ユニットテスト(約 240 件)・E2E テスト全件で Vite 7 と同一の結果を確認しました。

上のパフォーマンス比較表は Vite 8 適用後の数値ですが、特にビルド速度の改善は Webpack 4 からの差が歴然です。移行期間中に新しいメジャーバージョンが出るのは心理的にプレッシャーがありますが、「影響がないことを確認して取り込む」判断を迅速にできたのは、段階的移行で各フェーズのテストを整備していた恩恵だと感じています。

 

ハマりポイントと得られた知見

・Vue Router 5 は Vuetify 3 と非互換(2026年3月時点)

Vue Router 5 への更新も検討しましたが、Vuetify 3.12 が内部で next() コールバックパターンを使用しており、Vue Router 5 ではこれが非推奨化されているため全画面でワーニングが出ます。Vuetify 側の Issue が解決されるまで待つしかない状況です。

・Vuetify 4 はリリース直後で見送り

Vuetify 4.0.0 は 2026 年 2 月にリリースされましたが、CSS レイヤー、グリッドシステム、Typography と広範囲に破壊的変更があり、安定性も未知数だったため見送りました。3.x 系が並行リリースされているので急ぐ必要はないと判断しています。

・vee-validate の handleSubmit の挙動変更

vee-validate 4 の handleSubmit(callback) は、コールバックの第一引数にフォームフィールド値を渡します。既存のロジックでテーブルのソートオプションなどを引数に取っていた場合、値の判別ロジックが必要になります。

・Options API の維持は正解だった

約 70 ファイルを Composition API に書き換える工数をかけずに済んだのは大きいです。Vue 3 でも Options API は公式にサポートされていますし、既存コードの読みやすさも維持できました。「移行の目的は Vue 3 エコシステムに乗ること」であって、「コードをモダンに見せること」ではないので。

 

まとめ

Vue 2 → Vue 3 の移行は、Vue 本体よりも 周辺ライブラリの破壊的変更が大変でした。最終的な工数は約 1.5 人月(計画策定〜単体テストまで)です。特に Vuetify と vee-validate はほぼ全面書き直しです。

その結果として、**ビルド時間は約 7 倍高速(18.40 秒 → 2.67 秒)、開発サーバー起動は約 25 倍高速(6,154 ms → 241 ms)**になりました。日々の開発体験が劇的に変わるので、移行コストに見合うリターンは十分にあると感じています。

計画のポイントをまとめると:

  1. Vue 2.7 を中間ステップにする ― @vue/compat は Vuetify プロジェクトでは使えない
  2. Vite 移行を先にやる ― Webpack 4 は Node.js 22 と非互換
  3. Vue 3 + Vuetify 3 は同時移行が必須 ― 中間状態が存在しない
  4. 見た目の修正は最後にまとめてやる ― 起動を優先し、デグレ修正は後のフェーズに回す
  5. Options API は維持する ― 不必要な書き換えを避ける

これから移行を考えている方の参考になれば幸いです。

Miyagi

 

最後まで読んでいただき、ありがとうございました!
この記事が少しでもお役に立てれば嬉しいです。良い開発ライフを!

 

参考リンク