Vite の SSR とクライアントサイドの切り替え戦略

モダンな Web アプリケーション開発において、サーバーサイドレンダリング(SSR)とクライアントサイドの使い分けは重要な課題です。Vite を使った開発では、この切り替えをどのように実装するかが、アプリケーションのパフォーマンスとユーザー体験を大きく左右します。
本記事では、Vite の SSR とクライアントサイドの切り替えについて、実装手法を中心に詳しく解説していきます。実際のコード例とエラーハンドリングを含めて、実践的なアプローチをお伝えします。
Vite SSR の基本概念
Vite SSR とは
Vite の SSR(Server-Side Rendering)は、サーバー側で JavaScript を実行して HTML を生成し、クライアントに送信する技術です。従来のクライアントサイドレンダリング(CSR)とは異なり、初期表示が高速になり、SEO にも有利な特徴があります。
typescript// vite.config.ts - SSRの基本設定
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
ssr: {
// SSR用のエントリーポイントを指定
entry: 'src/entry-server.tsx',
// 外部化するパッケージを指定
external: ['react', 'react-dom'],
},
});
この設定により、Vite は SSR 用のビルドを生成し、サーバー側でレンダリングできるようになります。
クライアントサイドとの違い
SSR とクライアントサイドの主な違いは、JavaScript の実行環境にあります。
typescript// クライアントサイドでの実行
// ブラウザ環境でのみ動作
if (typeof window !== 'undefined') {
console.log('クライアントサイドで実行中');
}
// SSRでの実行
// サーバー環境でのみ動作
if (typeof window === 'undefined') {
console.log('サーバーサイドで実行中');
}
この違いを理解することで、適切な切り替え戦略を立てることができます。
なぜ切り替えが必要なのか
SSR とクライアントサイドの切り替えが必要な理由は、それぞれに最適なユースケースがあるからです。
SSR が適している場合:
- 初期表示速度が重要な場合
- SEO が重要な場合
- 静的コンテンツが多い場合
クライアントサイドが適している場合:
- インタラクティブな機能が多い場合
- リアルタイム更新が必要な場合
- 複雑な状態管理が必要な場合
動的インポートによる切り替え戦略
import()を使った動的インポート
動的インポートを使用することで、実行時にコンポーネントやモジュールを条件付きで読み込むことができます。
typescript// 動的インポートによる切り替え
const loadComponent = async (componentName: string) => {
try {
// 動的にコンポーネントをインポート
const module = await import(
`./components/${componentName}.tsx`
);
return module.default;
} catch (error) {
console.error(
'コンポーネントの読み込みに失敗しました:',
error
);
// フォールバックコンポーネントを返す
return () => <div>読み込みエラー</div>;
}
};
この方法により、必要な時だけコンポーネントを読み込むことができ、バンドルサイズを削減できます。
条件分岐による実装
実行環境に応じて、異なる実装を選択する条件分岐を実装します。
typescript// 環境に応じた条件分岐
const createRenderer = () => {
if (typeof window === 'undefined') {
// サーバーサイドでの実装
return {
render: (component: React.ReactElement) => {
return renderToString(component);
},
hydrate: () => {
throw new Error(
'サーバーサイドではハイドレーションできません'
);
},
};
} else {
// クライアントサイドでの実装
return {
render: (component: React.ReactElement) => {
return createRoot(
document.getElementById('root')!
).render(component);
},
hydrate: (component: React.ReactElement) => {
return hydrateRoot(
document.getElementById('root')!,
component
);
},
};
}
};
この実装により、同じ API で異なる環境に対応できます。
パフォーマンスの最適化
動的インポートを活用して、パフォーマンスを最適化します。
typescript// パフォーマンス最適化のための遅延読み込み
const LazyComponent = lazy(() =>
import('./HeavyComponent').then((module) => ({
default: module.default,
}))
);
// 使用例
function App() {
return (
<Suspense fallback={<div>読み込み中...</div>}>
<LazyComponent />
</Suspense>
);
}
この方法により、初期ロード時間を短縮し、ユーザー体験を向上させることができます。
環境変数による切り替え
import.meta.env の活用
Vite のimport.meta.env
を使用して、環境に応じた設定を管理します。
typescript// 環境変数による切り替え
const getConfig = () => {
return {
isSSR: import.meta.env.SSR,
isDev: import.meta.env.DEV,
isProd: import.meta.env.PROD,
apiUrl:
import.meta.env.VITE_API_URL ||
'http://localhost:3000',
};
};
// 使用例
const config = getConfig();
if (config.isSSR) {
console.log('SSRモードで実行中');
} else {
console.log('クライアントサイドで実行中');
}
この方法により、環境に応じた適切な設定を自動的に選択できます。
開発・本番環境での制御
開発環境と本番環境で異なる動作を実装します。
typescript// 環境別の設定管理
const getEnvironmentConfig = () => {
const baseConfig = {
enableDebug: false,
cacheTimeout: 30000,
maxRetries: 3,
};
if (import.meta.env.DEV) {
return {
...baseConfig,
enableDebug: true,
cacheTimeout: 5000,
};
}
return baseConfig;
};
// 使用例
const envConfig = getEnvironmentConfig();
if (envConfig.enableDebug) {
console.log('デバッグモードが有効です');
}
この実装により、開発時は詳細なログを出力し、本番環境ではパフォーマンスを最適化できます。
設定ファイルの管理
環境別の設定ファイルを管理して、切り替えを効率化します。
typescript// config/environment.ts
interface EnvironmentConfig {
ssr: {
enabled: boolean;
entryPoint: string;
};
client: {
enableHydration: boolean;
enablePrefetch: boolean;
};
}
const developmentConfig: EnvironmentConfig = {
ssr: {
enabled: true,
entryPoint: 'src/entry-server.tsx',
},
client: {
enableHydration: true,
enablePrefetch: false,
},
};
const productionConfig: EnvironmentConfig = {
ssr: {
enabled: true,
entryPoint: 'src/entry-server.tsx',
},
client: {
enableHydration: true,
enablePrefetch: true,
},
};
export const getConfig = (): EnvironmentConfig => {
return import.meta.env.PROD
? productionConfig
: developmentConfig;
};
この設定ファイルにより、環境に応じた適切な設定を一元管理できます。
プラグインを使った切り替え
Vite プラグインの作成
カスタム Vite プラグインを作成して、ビルド時に適切な切り替えを実装します。
typescript// plugins/ssr-switch.ts
import type { Plugin } from 'vite';
export function ssrSwitchPlugin(): Plugin {
return {
name: 'ssr-switch',
config(config, { command, mode }) {
// SSRモードの判定
const isSSR = mode === 'ssr';
return {
define: {
__IS_SSR__: JSON.stringify(isSSR),
__IS_CLIENT__: JSON.stringify(!isSSR),
},
};
},
transform(code, id) {
// 条件付きコードの変換
if (id.includes('.tsx') || id.includes('.ts')) {
return {
code: code
.replace(
/__IS_SSR__/g,
'typeof window === "undefined"'
)
.replace(
/__IS_CLIENT__/g,
'typeof window !== "undefined"'
),
map: null,
};
}
},
};
}
このプラグインにより、ビルド時に適切な条件分岐コードが生成されます。
ビルド時の処理分岐
ビルド時に環境に応じた処理を分岐させます。
typescript// vite.config.ts - プラグインの適用
import { defineConfig } from 'vite';
import { ssrSwitchPlugin } from './plugins/ssr-switch';
export default defineConfig(({ mode }) => {
const isSSR = mode === 'ssr';
return {
plugins: [ssrSwitchPlugin()],
build: {
rollupOptions: {
input: isSSR
? 'src/entry-server.tsx'
: 'src/main.tsx',
output: {
format: isSSR ? 'cjs' : 'es',
dir: isSSR ? 'dist/server' : 'dist/client',
},
},
},
};
});
この設定により、SSR とクライアントサイドで異なるビルド設定を適用できます。
カスタムプラグインの実装例
より高度なカスタムプラグインを実装します。
typescript// plugins/environment-switch.ts
import type { Plugin } from 'vite';
interface SwitchOptions {
ssrEntry?: string;
clientEntry?: string;
enableDebug?: boolean;
}
export function environmentSwitchPlugin(
options: SwitchOptions = {}
): Plugin {
const {
ssrEntry = 'src/entry-server.tsx',
clientEntry = 'src/main.tsx',
enableDebug = false,
} = options;
return {
name: 'environment-switch',
config(config, { mode }) {
const isSSR = mode === 'ssr';
if (enableDebug) {
console.log(`ビルドモード: ${mode}, SSR: ${isSSR}`);
}
return {
define: {
__BUILD_MODE__: JSON.stringify(mode),
__IS_SSR_BUILD__: JSON.stringify(isSSR),
},
build: {
rollupOptions: {
input: isSSR ? ssrEntry : clientEntry,
},
},
};
},
generateBundle(options, bundle) {
// バンドル生成時の処理
if (options.format === 'cjs') {
console.log('SSRバンドルを生成しました');
} else {
console.log('クライアントバンドルを生成しました');
}
},
};
}
このプラグインにより、より柔軟な環境切り替えが可能になります。
ハイドレーション戦略
サーバーサイドレンダリング後の処理
SSR で生成された HTML をクライアントサイドでハイドレーションする処理を実装します。
typescript// entry-client.tsx - クライアントサイドエントリーポイント
import { hydrateRoot } from 'react-dom/client';
import { App } from './App';
const container = document.getElementById('root');
if (!container) {
throw new Error('ルート要素が見つかりません');
}
// ハイドレーションの実行
hydrateRoot(container, <App />);
この処理により、SSR で生成された HTML にクライアントサイドの機能を統合できます。
クライアントサイドでの状態復元
サーバーサイドで生成された状態をクライアントサイドで復元します。
typescript// utils/hydration.ts
export const restoreState = () => {
try {
// サーバーサイドで生成された状態を取得
const stateElement = document.getElementById(
'__INITIAL_STATE__'
);
if (stateElement) {
const initialState = JSON.parse(
stateElement.textContent || '{}'
);
return initialState;
}
} catch (error) {
console.error('状態の復元に失敗しました:', error);
}
return {};
};
// 使用例
const initialState = restoreState();
const store = createStore(reducer, initialState);
この実装により、サーバーサイドとクライアントサイドで一貫した状態を維持できます。
エラーハンドリング
ハイドレーション時のエラーを適切に処理します。
typescript// utils/error-boundary.tsx
import React, {
Component,
ErrorInfo,
ReactNode,
} from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error(
'ハイドレーションエラー:',
error,
errorInfo
);
// エラーをログに記録
if (typeof window !== 'undefined') {
// クライアントサイドでのエラーログ
console.error('クライアントサイドエラー:', error);
}
}
render() {
if (this.state.hasError) {
return (
this.props.fallback || (
<div>
<h2>エラーが発生しました</h2>
<p>ページを再読み込みしてください</p>
</div>
)
);
}
return this.props.children;
}
}
このエラーバウンダリーにより、ハイドレーション時のエラーを適切にキャッチし、ユーザーに分かりやすいメッセージを表示できます。
パフォーマンス最適化
バンドルサイズの削減
動的インポートとコード分割を活用してバンドルサイズを削減します。
typescript// utils/code-splitting.ts
export const createLazyComponent = (
importFn: () => Promise<any>
) => {
return lazy(() =>
importFn().catch((error) => {
console.error(
'コンポーネントの読み込みに失敗:',
error
);
// フォールバックコンポーネントを返す
return { default: () => <div>読み込みエラー</div> };
})
);
};
// 使用例
const LazyHeavyComponent = createLazyComponent(
() => import('./HeavyComponent')
);
const LazyChartComponent = createLazyComponent(
() => import('./ChartComponent')
);
この実装により、必要な時だけコンポーネントを読み込み、バンドルサイズを削減できます。
読み込み速度の向上
プリロードとプリフェッチを活用して読み込み速度を向上させます。
typescript// utils/preload.ts
export const preloadComponent = (
importFn: () => Promise<any>
) => {
// バックグラウンドでプリロード
const promise = importFn();
return {
component: lazy(() => promise),
preload: () => promise,
};
};
// 使用例
const { component: LazyModal, preload: preloadModal } =
preloadComponent(() => import('./Modal'));
// ユーザーがボタンにホバーした時にプリロード
const handleMouseEnter = () => {
preloadModal();
};
この方法により、ユーザーの操作を予測して事前にコンポーネントを読み込み、体感速度を向上させることができます。
キャッシュ戦略
適切なキャッシュ戦略を実装してパフォーマンスを最適化します。
typescript// utils/cache.ts
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
}
class Cache {
private cache = new Map<string, CacheEntry<any>>();
set<T>(key: string, data: T, ttl: number = 300000): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
ttl,
});
}
get<T>(key: string): T | null {
const entry = this.cache.get(key);
if (!entry) return null;
const isExpired =
Date.now() - entry.timestamp > entry.ttl;
if (isExpired) {
this.cache.delete(key);
return null;
}
return entry.data;
}
clear(): void {
this.cache.clear();
}
}
// グローバルキャッシュインスタンス
export const globalCache = new Cache();
// 使用例
const fetchData = async (url: string) => {
const cacheKey = `data:${url}`;
const cached = globalCache.get(cacheKey);
if (cached) {
return cached;
}
const response = await fetch(url);
const data = await response.json();
globalCache.set(cacheKey, data, 60000); // 1分間キャッシュ
return data;
};
このキャッシュ戦略により、重複した API 呼び出しを避け、アプリケーションの応答性を向上させることができます。
まとめ
Vite の SSR とクライアントサイドの切り替え戦略について、実装手法を中心に詳しく解説しました。
動的インポートによる条件分岐、環境変数を使った設定管理、カスタムプラグインの作成、適切なハイドレーション戦略、そしてパフォーマンス最適化まで、実践的なアプローチをお伝えしました。
これらの手法を組み合わせることで、ユーザー体験とパフォーマンスの両方を最適化した Web アプリケーションを構築できます。特に、エラーハンドリングとキャッシュ戦略は、本番環境での安定性を確保するために重要な要素です。
Vite の柔軟性を活かして、プロジェクトの要件に応じた最適な切り替え戦略を選択し、実装していくことをお勧めします。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来