拡張性の高いWebアプリに潜む隠れたコスト
2021年当時、私は大規模なECプラットフォームを構築するチームを率いていました。私たちはVue 2を使用した標準的なシングルページアプリケーション(SPA)アーキテクチャを採用しました。ハイエンドのMacBook上では、そのサイトは夢のように快適に動作していました。しかし、いざ本番環境にリリースすると、LighthouseのSEOスコアは45/100、ミドルレンジのAndroid端末でのFirst Contentful Paint(FCP)は5秒という厳しい現実に直面しました。
検索順位は皆無でした。アプリが完全にブラウザ上で動作していたため、Googleのクローラーには空の <div id="app"></div> しか見えていなかったのです。3G回線のユーザーは、2.5MBのJavaScriptバンドルがダウンロードされ解析される間、数秒間も真っ白な画面を見つめていました。その時、私たちは標準的なSPAが必ずしも一般公開向けの成長を目指すサービスにとって最適なツールではないことに気づきました。
従来のSPAが大規模開発で苦戦する理由
問題はクライアントサイドレンダリング(CSR)モデルそのものにあります。この構成では、サーバーは単なるファイルホストとして機能します。中身のないHTMLシェルとスクリプトタグを送信し、残りの重い処理はすべてユーザーのデバイスに委ねられます。ユーザーのプロセッサが非力だったり、接続が不安定だったりすると、アプリは壊れているかのように感じられます。
メンテナンスもまた別の障害となります。Vue 2の旧来のOptions APIでは、機能ごとではなくデータ型ごとにコードを整理せざるを得ません。ショッピングカートのデバッグをしようとして、ロジックが1つのファイルの50行目、200行目、450行目に分散している状況を想像してみてください。これは技術的負債の温床です。Vuexによる状態管理も、たった1つの文字列を更新するために4つの異なるファイルを必要とすることが多く、煩雑さを増大させます。
転換点:CSR vs. SSR vs. Composition API
現代のWeb開発は、よりバランスの取れたアプローチへと移行しています。状況は以下のように変化しました。
- Vue 2 + Vuex(レガシーな手法): 社内ツールには信頼性がありますが、SEOの面では不利です。ロジックの再利用にはMixinsが使われますが、プロパティの出所が不明確になりがちです。
- Vue 3 + Composition API(ロジックの改善): コードを機能ごとにグループ化できます。
setupとcomposablesを使用することで、コンポーネントのボイラープレートを最大40%削減できたチームもあります。 - Nuxt 3(パフォーマンスの改善): Nuxtは標準でサーバーサイドレンダリング(SSR)をサポートしています。サーバー側でHTMLを生成するため、ユーザーは最初のバイトが届いた瞬間にコンテンツを目にすることができます。
実証済みのスタック:Nuxt 3、Composition API、Pinia
いくつかのエンタープライズプロジェクトを移行した結果、この特定の組み合わせがスピードと開発者の精神衛生の面で最高のバランスをもたらすことがわかりました。これは単に新しいツールを使うということではなく、より予測可能なワークフローを構築することを意味します。具体的な実装を見ていきましょう。
1. Nuxtによるプロジェクトの立ち上げ
Nuxtは、ViteやWebpackを手動で設定する際の「設定疲れ」を解消してくれます。本番環境に対応した環境を数秒で構築できます。
npx nuxi@latest init my-modern-app
cd my-modern-app
npm install
500行もある router.js ファイルを管理する必要はありません。Nuxtはファイルベースのルーティングを採用しています。pages/products/[id].vue を作成すれば、フレームワークが自動的に動的ルートを生成します。この構造により、ルート数が10から100以上に増えてもプロジェクトを整理された状態に保てます。
2. <script setup> によるクリーンなロジック
<script setup> 構文は、Vue 3における最も重要な改善点です。これによりコンポーネントはより軽量で読みやすくなります。オブジェクトの異なるプロパティ間を行ったり来たりする代わりに、ロジックを線形に記述できます。
<script setup>
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
</script>
<template>
<div>
<p>現在のカウント: {{ count }}</p>
<p>2倍の値: {{ doubleCount }}</p>
<button @click="increment">増やす</button>
</div>
</template>
このアプローチにより、複雑なロジックを「Composables(コンポーザブル)」に切り出すことができます。これらはUIコンポーネントをビュー層に集中させるための、再利用可能な特殊な関数と考えてください。
3. Piniaによる効率的な状態管理
PiniaはVuexの軽量な後継ツールです。Vue 2で常に摩擦の原因となっていたmutationsが不要になります。グローバルでリアクティブな状態という利点を保ちつつ、標準的なJavaScriptオブジェクトを扱うような感覚で利用できます。
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
name: 'アレックス',
isLoggedIn: false
}),
actions: {
login(userName) {
this.name = userName;
this.isLoggedIn = true;
}
}
})
コンポーネントからのアクセスは直接的です。mapGetters や mapActions はもう必要ありません。ストアをインポートしてそのまま使うだけです。
4. インテリジェントなデータ取得
Nuxt 3のデータ取得は、「二重フェッチ」問題を防ぐように設計されています。標準的なSPAでは、サーバーがすでに提供できたはずのデータをクライアントが再度リクエストすることがよくあります。Nuxtの useFetch コンポーザブルは、この移行をスムーズに処理します。
<script setup>
const { data: products, pending } = await useFetch('/api/products', {
lazy: true
})
</script>
<template>
<div v-if="pending">在庫情報を更新中...</div>
<ul v-else>
<li v-for="item in products" :key="item.id">{{ item.name }}</li>
</ul>
</template>
検索エンジンがこのページにアクセスすると、サーバーはAPIのレスポンスを待ち、その結果をHTMLに直接注入します。これにより、コンテンツが即座にインデックスされることが保証されます。
デプロイ戦略の選択
NuxtはNitroエンジンを使用しているため、特定のホスティングプロバイダーに縛られることはありません。本番環境には主に3つのパスがあります。
- 静的サイト生成 (SSG): ブログやドキュメントに最適です。
npx nuxi generateを使用してサイトをビルドし、NetlifyなどのCDNにデプロイすれば、ほぼゼロコストで運用できます。 - 従来のNode.js: 常駐サーバーが必要な動的アプリに最適です。PM2などのプロセス管理ツールを使用して、任意のVPS上で
.outputフォルダを実行できます。 - Edge/Serverless: VercelやCloudflare Workersなどのプラットフォームにデプロイします。コードがユーザーの地理的に近い場所で実行されるため、レイテンシを最小限に抑えられます。
標準的なVueのSPAからNuxtベースのアーキテクチャへの移行には学習コストがかかりますが、その成果は明らかです。SEOの向上、体感的なパフォーマンスの高速化、および自身の重みで崩壊することのないコードベースを手に入れることができます。これらのパターンを採用することで、現代のインターネットに真に対応したWebアプリケーションを構築できるのです。

