T-CREATOR

React から Preact への移行ガイド - 互換性と差分を徹底解説

React から Preact への移行ガイド - 互換性と差分を徹底解説

モダンなWebアプリケーション開発において、パフォーマンス最適化は最優先課題の一つです。特にモバイルデバイスでの表示速度やバンドルサイズの削減は、ユーザーエクスペリエンスに直結する重要な要素となっています。

そんな中で注目を集めているのが、Reactの軽量代替ライブラリである「Preact」への移行です。Preactは、Reactとほぼ同じAPIを持ちながら、わずか3KBという驚異的な軽量性を実現しており、既存のReactプロジェクトから比較的簡単に移行できる魅力的な選択肢となっています。

しかし、移行には技術的な課題や互換性の制限もあります。本記事では、ReactからPreactへの移行を検討している開発者の皆様に向けて、実践的なガイドラインと具体的な実装方法をご紹介いたします。

背景

Preactとは何か

Preactは、Jason Miller氏によって開発されたJavaScriptライブラリで、Reactの軽量版として設計されています。「Fast 3kB alternative to React with the same modern API」というキャッチフレーズの通り、Reactと同等の機能性を保ちながら、極めて小さなファイルサイズを実現しているのが最大の特徴です。

Preactの主要な特徴を以下の表でまとめました。

項目PreactReact
バンドルサイズ約3KB約43KB
レンダリング性能高速高速
API互換性95%以上100%
エコシステム限定的豊富
TypeScript対応ありあり

Preactが軽量である理由は、Reactの中核機能に焦点を絞り、不要な機能を削除しているからです。具体的には、Synthetic Eventsの簡素化、不要なポリフィルの除去、最適化されたDiffアルゴリズムの採用などが挙げられます。

mermaidflowchart TB
    react[React<br/>43KB] --> |API互換| preact[Preact<br/>3KB]
    react --> |機能削減| core[コア機能のみ]
    core --> preact
    react --> |最適化| diff[高速Diffアルゴリズム]
    diff --> preact
    react --> |ポリフィル除去| minimal[最小限の実装]
    minimal --> preact

上図のように、PreactはReactの豊富な機能から本当に必要なコア機能のみを抽出し、軽量化を実現しています。

React との関係性

PreactとReactの関係は、互換性を重視した代替実装として位置づけられます。Preactは単なるReactのクローンではなく、同じ開発体験を提供しながら、より効率的な実装を目指して設計されています。

APIレベルでの互換性について詳しく見てみましょう。

javascript// React と Preact で同じように動作するコンポーネント例
import { useState, useEffect } from 'react'; // または 'preact/hooks'

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(response => response.json())
      .then(userData => {
        setUser(userData);
        setLoading(false);
      });
  }, [userId]);

  if (loading) return <div>読み込み中...</div>;
  if (!user) return <div>ユーザーが見つかりません</div>;

  return (
    <div className="user-profile">
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

上記のコンポーネントは、ReactでもPreactでも全く同じように動作します。この高い互換性により、既存のReactプロジェクトをPreactに移行する際の学習コストを大幅に削減できます。

mermaidsequenceDiagram
    participant Dev as 開発者
    participant React as React App
    participant Preact as Preact App
    participant User as エンドユーザー

    Dev->>React: 既存コード作成
    Dev->>Preact: 移行作業実施
    Note over React,Preact: 同じAPIを使用
    Preact->>User: 軽量なバンドル配信
    User->>Preact: 高速な初回読み込み

この図が示すように、開発者は同じAPIを使用しながら、エンドユーザーには軽量化されたアプリケーションを提供できるのです。

移行を検討する理由

ReactからPreactへの移行を検討する主な理由をご紹介します。

1. バンドルサイズの大幅削減

最も顕著な効果は、バンドルサイズの削減です。Reactの約43KBに対してPreactは約3KBと、実に90%以上のサイズ削減が可能になります。

javascript// バンドル分析の実例(webpack-bundle-analyzer結果)
const bundleAnalysis = {
  before: {
    react: '42.8KB',
    reactDom: '132.3KB',
    total: '175.1KB'
  },
  after: {
    preact: '3.2KB',
    preactCompat: '4.1KB', // 互換性レイヤー使用時
    total: '7.3KB'
  },
  reduction: '95.8%'
};

2. 初回読み込み時間の改善

軽量化により、特にモバイルデバイスでの初回読み込み時間が大幅に改善されます。

接続環境React読み込み時間Preact読み込み時間改善率
3G2.1秒0.3秒85.7%
4G0.8秒0.1秒87.5%
WiFi0.2秒0.03秒85.0%

3. レガシーブラウザでの動作性能

Preactは古いブラウザでも軽快に動作し、幅広いユーザーに対応できます。

javascript// Preactの軽量実装により、IE11でも快適な動作を実現
const performanceMetrics = {
  ie11: {
    react: { firstPaint: '3.2s', interactive: '4.8s' },
    preact: { firstPaint: '1.1s', interactive: '1.7s' }
  },
  chrome: {
    react: { firstPaint: '0.8s', interactive: '1.2s' },
    preact: { firstPaint: '0.3s', interactive: '0.5s' }
  }
};

これらの理由から、特にパフォーマンスが重要なWebアプリケーションや、モバイルファーストなプロダクトにおいて、Preactへの移行は非常に魅力的な選択肢となっています。

図で理解できる要点

  • PreactはReactの90%以上軽量化を実現
  • API互換性を保ちつつパフォーマンス向上
  • 移行により初回読み込み時間を大幅短縮

課題

React から Preact 移行時の主な課題

ReactからPreactへの移行は多くのメリットをもたらしますが、同時に技術的な課題も存在します。移行を成功させるためには、これらの課題を事前に理解し、適切な対策を講じることが重要です。

1. サードパーティライブラリの互換性問題

最も頻繁に発生する課題は、Reactエコシステムのライブラリとの互換性です。

javascript// 問題のあるライブラリの例
import { Tooltip } from 'react-bootstrap'; // Preactで動作しない可能性
import { DatePicker } from 'react-datepicker'; // 内部でReact固有APIを使用
import { Router } from 'react-router-dom'; // 完全互換ではない

// 回避策:preact/compat使用時の設定
// webpack.config.js
module.exports = {
  resolve: {
    alias: {
      "react": "preact/compat",
      "react-dom": "preact/compat"
    }
  }
};

2. 開発ツールとの統合課題

React Developer Toolsや一部のテストツールは、Preactでは完全に機能しない場合があります。

javascript// Jest設定での互換性問題対応例
// jest.config.js
module.exports = {
  moduleNameMapping: {
    '^react$': 'preact/compat',
    '^react-dom$': 'preact/compat',
    '^react-dom/test-utils$': 'preact/test-utils',
    '^react-test-renderer$': 'preact/test-utils'
  },
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js']
};

3. チーム開発での学習コスト

開発チーム全体での技術習得と、既存の開発フローの調整が必要になります。

互換性の制限事項

Preactには、Reactとの互換性において制限があることを理解しておく必要があります。

1. Synthetic Eventsの簡素化

PreactのSynthetic Eventsは、Reactより簡素な実装となっています。

javascript// React:完全なSynthetic Event
function ReactComponent() {
  const handleClick = (event) => {
    event.persist(); // Reactでは利用可能
    event.nativeEvent; // すべてのブラウザイベントにアクセス可能
  };
  
  return <button onClick={handleClick}>クリック</button>;
}

// Preact:簡素化されたイベント
function PreactComponent() {
  const handleClick = (event) => {
    // event.persist()は利用不可
    // 一部のevent.nativeEventプロパティが制限される
    console.log(event.target.value); // 基本的な操作は同じ
  };
  
  return <button onClick={handleClick}>クリック</button>;
}

2. Context APIの制限

React 16.3以降のContext APIは基本的にサポートされていますが、一部の高度な機能に制限があります。

javascript// React Context(完全サポート)
const ThemeContext = React.createContext('light');

// Preact Context(基本機能のみ)
import { createContext } from 'preact';
const ThemeContext = createContext('light');

// 使用方法は同じ
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <ThemedComponent />
    </ThemeContext.Provider>
  );
}

3. Ref取得の違い

Refオブジェクトのcurrentプロパティへのアクセスタイミングに違いがある場合があります。

javascript// React
function ReactRef() {
  const inputRef = useRef(null);
  
  useEffect(() => {
    inputRef.current.focus(); // 確実にアクセス可能
  }, []);
  
  return <input ref={inputRef} />;
}

// Preact(注意が必要な場合)
function PreactRef() {
  const inputRef = useRef(null);
  
  useEffect(() => {
    // タイミングによってはnullの可能性
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []);
  
  return <input ref={inputRef} />;
}
mermaidflowchart TD
    migration[移行開始] --> compat{preact/compat<br/>使用?}
    compat -->|Yes| high[高互換性<br/>95%以上]
    compat -->|No| medium[中程度互換性<br/>80-90%]
    
    high --> libs[サードパーティ<br/>ライブラリ確認]
    medium --> manual[手動書き換え<br/>必要]
    
    libs --> test[テスト実行]
    manual --> test
    test --> issues{問題発見?}
    issues -->|Yes| fix[個別対応]
    issues -->|No| success[移行完了]
    fix --> test

パフォーマンス考慮点

Preactへの移行において、パフォーマンス面で注意すべき点があります。

1. preact/compatのオーバーヘッド

互換性レイヤーを使用する場合、純粋なPreactよりもサイズが増加します。

javascript// バンドルサイズ比較
const bundleSizes = {
  react: '175KB',           // React + ReactDOM
  preactPure: '3KB',       // 純粋なPreact
  preactCompat: '7KB',     // preact/compat使用時
  reduction: {
    pure: '98.3%',         // 純粋なPreactでの削減率
    compat: '96.0%'        // preact/compat使用時の削減率
  }
};

2. 初期レンダリングの最適化

Preactは軽量ですが、初期レンダリングの最適化手法が若干異なります。

javascript// React:React.memo使用
import React from 'react';
const OptimizedComponent = React.memo(({ data }) => {
  return <div>{data.title}</div>;
});

// Preact:memo使用
import { memo } from 'preact/compat';
const OptimizedComponent = memo(({ data }) => {
  return <div>{data.title}</div>;
});

3. SSR(Server-Side Rendering)の制約

PreactのSSRは、ReactのSSRより機能が限定的です。

javascript// React SSR(豊富な機能)
import { renderToString } from 'react-dom/server';

// Preact SSR(基本機能)
import { render } from 'preact-render-to-string';

// Next.jsでのPreact使用時の制限
const nextConfig = {
  experimental: {
    runtime: 'nodejs', // edge runtimeは制限あり
  }
};

これらの課題を理解した上で、次のセクションでは具体的な解決策をご紹介いたします。

図で理解できる要点

  • サードパーティライブラリとの互換性が主要課題
  • preact/compatで多くの問題は解決可能
  • パフォーマンス最適化手法に若干の違いあり

解決策

Preact/compat による段階的移行

ReactからPreactへの移行において、最も実用的なアプローチは「preact/compat」を活用した段階的移行です。この互換性レイヤーを使用することで、既存のReactコードをほぼそのままPreactで動作させることができます。

1. preact/compatの導入手順

まず、プロジェクトにPreactとpreact/compatをインストールします。

bash# 既存のReactパッケージを削除
yarn remove react react-dom

# Preactパッケージをインストール
yarn add preact
yarn add --dev @preact/preset-vite # Vite使用時

2. エイリアス設定による透過的な移行

ビルドツールでエイリアスを設定し、ReactインポートをPreactに自動的にリダイレクトします。

javascript// vite.config.js(Vite使用時)
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';

export default defineConfig({
  plugins: [preact()],
  resolve: {
    alias: {
      'react': 'preact/compat',
      'react-dom': 'preact/compat'
    }
  }
});
javascript// webpack.config.js(Webpack使用時)
module.exports = {
  resolve: {
    alias: {
      'react': 'preact/compat',
      'react-dom': 'preact/compat',
      'react-dom/test-utils': 'preact/test-utils',
      'react-test-renderer': 'preact/test-utils'
    }
  }
};

3. 段階的移行の戦略

移行を段階的に進めることで、リスクを最小化できます。

mermaidflowchart LR
    phase1[フェーズ1<br/>preact/compat導入] --> phase2[フェーズ2<br/>コンポーネント書き換え]
    phase2 --> phase3[フェーズ3<br/>純粋Preact移行]
    
    phase1 --> test1[テスト実行]
    phase2 --> test2[段階的テスト]
    phase3 --> test3[最終検証]
    
    test1 --> fix1[問題修正]
    test2 --> fix2[個別対応]
    test3 --> complete[移行完了]

各フェーズでの具体的な作業内容をご紹介します。

javascript// フェーズ1:最小限の変更で動作確認
// package.json
{
  "dependencies": {
    "preact": "^10.19.0"
  },
  "devDependencies": {
    "@preact/preset-vite": "^2.8.0"
  }
}

// 既存のReactコンポーネントはそのまま動作
import React, { useState } from 'react'; // 自動的にpreact/compatにリダイレクト

function ExistingComponent() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>増加</button>
    </div>
  );
}

設定ファイルの調整方法

各種設定ファイルをPreact用に調整する具体的な方法をご説明します。

1. TypeScript設定の調整

TypeScriptプロジェクトでは、型定義の調整が必要です。

json// tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact",
    "types": ["node", "vite/client"],
    "paths": {
      "react": ["./node_modules/preact/compat"],
      "react-dom": ["./node_modules/preact/compat"]
    }
  }
}
typescript// types/preact.d.ts(型定義ファイル)
declare module 'preact/compat' {
  export * from 'react';
  export { render } from 'preact';
}

// グローバル型定義の拡張
declare global {
  namespace JSX {
    interface IntrinsicElements {
      [elemName: string]: any;
    }
  }
}

2. ESLint設定の更新

ESLintルールをPreact用に調整します。

json// .eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "@typescript-eslint/recommended"
  ],
  "plugins": ["@typescript-eslint"],
  "settings": {
    "react": {
      "pragma": "h",
      "version": "detect"
    }
  },
  "rules": {
    "react/react-in-jsx-scope": "off",
    "react/prop-types": "off"
  }
}

3. テスト設定の調整

Jestやその他のテストフレームワークでの設定調整が重要です。

javascript// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
  moduleNameMapping: {
    '^react$': 'preact/compat',
    '^react-dom$': 'preact/compat',
    '^react-dom/test-utils$': 'preact/test-utils'
  },
  transform: {
    '^.+\\.(ts|tsx)$': 'ts-jest',
    '^.+\\.(js|jsx)$': 'babel-jest'
  }
};
javascript// src/setupTests.js
import { configure } from '@testing-library/react';
import 'jest-dom/extend-expect';

// Preact用のテスト環境設定
configure({ testIdAttribute: 'data-testid' });

ビルドツールの最適化

Preactを最大限活用するためのビルド最適化設定をご紹介します。

1. Viteでの最適化設定

javascript// vite.config.js
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';

export default defineConfig({
  plugins: [preact()],
  build: {
    rollupOptions: {
      external: ['react', 'react-dom'], // バンドルから除外
      output: {
        manualChunks: {
          'preact-vendor': ['preact', 'preact/hooks'],
          'router': ['preact-router']
        }
      }
    },
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true, // console.log除去
        drop_debugger: true
      }
    }
  },
  resolve: {
    alias: {
      'react': 'preact/compat',
      'react-dom': 'preact/compat'
    }
  }
});

2. Webpackでの最適化設定

javascript// webpack.config.js
const path = require('path');

module.exports = {
  resolve: {
    alias: {
      'react': 'preact/compat',
      'react-dom': 'preact/compat'
    }
  },
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        preact: {
          test: /[\\/]node_modules[\\/](preact)[\\/]/,
          name: 'preact',
          chunks: 'all'
        }
      }
    }
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production')
    })
  ]
};

3. バンドルサイズ最適化の実例

最適化前後のバンドルサイズ比較を見てみましょう。

javascript// バンドル分析結果の例
const optimizationResults = {
  before: {
    main: '245KB',
    vendor: '180KB',
    total: '425KB'
  },
  after: {
    main: '245KB',     // アプリケーションコードは同じ
    vendor: '12KB',    // React → Preactに変更
    total: '257KB'     // 39%削減
  },
  improvement: {
    vendorReduction: '93.3%',
    totalReduction: '39.5%',
    loadTimeImprovement: '45%'
  }
};

4. 開発環境での最適化

開発効率を向上させるための設定も重要です。

javascript// 開発環境用設定
const devConfig = {
  devServer: {
    hot: true,           // ホットリロード有効
    overlay: true        // エラーオーバーレイ表示
  },
  resolve: {
    alias: {
      'react-dom': 'preact/compat',
      'react': 'preact/compat'
    }
  },
  devtool: 'eval-source-map' // 高速なソースマップ
};

これらの設定により、Preactの軽量性を最大限活用しながら、開発体験も向上させることができます。

図で理解できる要点

  • preact/compatで既存コードをほぼそのまま移行可能
  • ビルドツール設定でエイリアス指定が重要
  • 段階的移行により安全な移行を実現

具体例

Next.js プロジェクトの移行手順

Next.jsプロジェクトをPreactに移行する具体的な手順をステップバイステップでご説明します。

1. プロジェクトの準備と依存関係の更新

まず、既存のNext.jsプロジェクトでPreactを使用するための設定を行います。

bash# 必要なパッケージをインストール
yarn add preact preact-render-to-string
yarn add --dev next-plugin-preact

2. Next.js設定ファイルの更新

javascript// next.config.js
const withPreact = require('next-plugin-preact');

const nextConfig = {
  experimental: {
    // Preact使用時の最適化設定
    esmExternals: true,
    modern: true
  },
  webpack: (config, { dev, isServer }) => {
    // PreactとReactのエイリアス設定
    config.resolve.alias = {
      ...config.resolve.alias,
      'react': 'preact/compat',
      'react-dom': 'preact/compat'
    };

    // 開発環境でのファストリフレッシュ対応
    if (dev && !isServer) {
      const originalEntry = config.entry;
      config.entry = async () => {
        const entries = await originalEntry();
        if (entries['main.js']) {
          entries['main.js'].unshift('./src/utils/preact-devtools.js');
        }
        return entries;
      };
    }

    return config;
  }
};

module.exports = withPreact(nextConfig);

3. TypeScript設定の調整

json// tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "es6"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "jsxImportSource": "preact",
    "paths": {
      "@/*": ["./src/*"],
      "react": ["./node_modules/preact/compat"],
      "react-dom": ["./node_modules/preact/compat"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

4. カスタムAppコンポーネントの更新

typescript// pages/_app.tsx
import type { AppProps } from 'next/app';
import { useEffect } from 'preact/hooks';

// グローバルスタイル
import '../styles/globals.css';

function MyApp({ Component, pageProps }: AppProps) {
  useEffect(() => {
    // Preact開発者ツールの初期化(開発環境のみ)
    if (process.env.NODE_ENV === 'development') {
      import('preact/devtools');
    }
  }, []);

  return <Component {...pageProps} />;
}

export default MyApp;

5. SSR対応の実装

Next.jsのSSR機能をPreactで使用するための設定です。

typescript// pages/_document.tsx
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { render } from 'preact-render-to-string';

class MyDocument extends Document {
  static async getInitialProps(ctx: any) {
    const originalRenderPage = ctx.renderPage;
    
    ctx.renderPage = () =>
      originalRenderPage({
        enhanceApp: (App: any) => App,
        enhanceComponent: (Component: any) => Component,
      });

    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps };
  }

  render() {
    return (
      <Html lang="ja">
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

6. API Routes の動作確認

typescript// pages/api/health.ts
import type { NextApiRequest, NextApiResponse } from 'next';

type HealthData = {
  status: string;
  framework: string;
  timestamp: string;
};

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<HealthData>
) {
  res.status(200).json({
    status: 'OK',
    framework: 'Next.js with Preact',
    timestamp: new Date().toISOString()
  });
}

Create React App からの移行

Create React App(CRA)からPreactへの移行は、設定の自由度が高いため、より詳細な調整が可能です。

1. CRAのイジェクト(推奨方法)

bash# まず現在の状態をバックアップ
git add .
git commit -m "移行前のバックアップ"

# CRAをイジェクト(必要に応じて)
yarn eject

2. 設定ファイルの完全書き換え

イジェクト後のWebpack設定をPreact用に最適化します。

javascript// config/webpack.config.js(抜粋)
const path = require('path');

module.exports = function(webpackEnv) {
  const isEnvDevelopment = webpackEnv === 'development';
  const isEnvProduction = webpackEnv === 'production';

  return {
    resolve: {
      alias: {
        // ReactをPreactに置き換え
        'react': path.resolve(__dirname, '../node_modules/preact/compat'),
        'react-dom': path.resolve(__dirname, '../node_modules/preact/compat'),
        'react-dom/test-utils': path.resolve(__dirname, '../node_modules/preact/test-utils'),
        'react-test-renderer': path.resolve(__dirname, '../node_modules/preact/test-utils')
      }
    },
    optimization: {
      splitChunks: {
        chunks: 'all',
        cacheGroups: {
          preact: {
            test: /[\\/]node_modules[\\/](preact)[\\/]/,
            name: 'preact',
            chunks: 'all',
            priority: 10
          }
        }
      }
    }
  };
};

3. イジェクトしない方法(CRACO使用)

CRAをイジェクトせずにPreactを使用する方法もあります。

bash# CRACOをインストール
yarn add --dev @craco/craco
javascript// craco.config.js
const path = require('path');

module.exports = {
  webpack: {
    alias: {
      'react': path.resolve(__dirname, 'node_modules/preact/compat'),
      'react-dom': path.resolve(__dirname, 'node_modules/preact/compat')
    },
    configure: (webpackConfig) => {
      // バンドルサイズ最適化
      if (process.env.NODE_ENV === 'production') {
        webpackConfig.optimization.splitChunks.cacheGroups.preact = {
          test: /[\\/]node_modules[\\/](preact)[\\/]/,
          name: 'preact',
          chunks: 'all'
        };
      }
      return webpackConfig;
    }
  }
};
json// package.json(scriptsセクション)
{
  "scripts": {
    "start": "craco start",
    "build": "craco build",
    "test": "craco test"
  }
}

既存コンポーネントの書き換え実例

実際のReactコンポーネントをPreact向けに最適化する具体例をご紹介します。

1. クラスコンポーネントの書き換え

typescript// React版のクラスコンポーネント
import React, { Component } from 'react';

interface UserListState {
  users: User[];
  loading: boolean;
  error: string | null;
}

class UserListReact extends Component<{}, UserListState> {
  constructor(props: {}) {
    super(props);
    this.state = {
      users: [],
      loading: true,
      error: null
    };
  }

  async componentDidMount() {
    try {
      const response = await fetch('/api/users');
      const users = await response.json();
      this.setState({ users, loading: false });
    } catch (error) {
      this.setState({ error: error.message, loading: false });
    }
  }

  render() {
    const { users, loading, error } = this.state;
    
    if (loading) return <div>読み込み中...</div>;
    if (error) return <div>エラー: {error}</div>;
    
    return (
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    );
  }
}
typescript// Preact版への書き換え(Hooks使用)
import { useState, useEffect } from 'preact/hooks';

interface User {
  id: number;
  name: string;
  email: string;
}

function UserListPreact() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        const response = await fetch('/api/users');
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const userData = await response.json();
        setUsers(userData);
      } catch (err) {
        setError(err instanceof Error ? err.message : '不明なエラー');
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []);

  if (loading) {
    return (
      <div className="loading-spinner">
        <span>読み込み中...</span>
      </div>
    );
  }

  if (error) {
    return (
      <div className="error-message">
        <h3>エラーが発生しました</h3>
        <p>{error}</p>
        <button onClick={() => window.location.reload()}>
          再読み込み
        </button>
      </div>
    );
  }

  return (
    <div className="user-list">
      <h2>ユーザー一覧</h2>
      <ul>
        {users.map(user => (
          <li key={user.id} className="user-item">
            <span className="user-name">{user.name}</span>
            <span className="user-email">{user.email}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default UserListPreact;

2. カスタムHooksの活用

PreactでもカスタムHooksを効果的に活用できます。

typescript// カスタムHookの実装例
import { useState, useEffect } from 'preact/hooks';

// データフェッチ用のカスタムHook
function useApi<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let isCancelled = false;

    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`HTTPエラー: ${response.status}`);
        }
        
        const result = await response.json();
        
        if (!isCancelled) {
          setData(result);
        }
      } catch (err) {
        if (!isCancelled) {
          setError(err instanceof Error ? err.message : '不明なエラー');
        }
      } finally {
        if (!isCancelled) {
          setLoading(false);
        }
      }
    };

    fetchData();

    // クリーンアップ関数
    return () => {
      isCancelled = true;
    };
  }, [url]);

  return { data, loading, error };
}

// カスタムHookを使用したコンポーネント
function UserProfile({ userId }: { userId: string }) {
  const { data: user, loading, error } = useApi<User>(`/api/users/${userId}`);

  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;
  if (!user) return <div>ユーザーが見つかりません</div>;

  return (
    <div className="user-profile">
      <h1>{user.name}</h1>
      <p>メール: {user.email}</p>
    </div>
  );
}
mermaidsequenceDiagram
    participant Dev as 開発者
    participant Build as ビルドツール
    participant Bundle as バンドル
    participant Browser as ブラウザ

    Dev->>Build: React → Preact移行
    Build->>Build: エイリアス設定適用
    Build->>Bundle: 最適化済みバンドル生成
    Note over Bundle: 90%以上軽量化
    Bundle->>Browser: 高速配信
    Browser->>Browser: 高速初期表示

3. パフォーマンス最適化の実装

Preactの軽量性を活かしたパフォーマンス最適化の例です。

typescript// メモ化を活用した最適化
import { memo } from 'preact/compat';
import { useCallback, useMemo } from 'preact/hooks';

interface ProductItemProps {
  product: Product;
  onAddToCart: (productId: number) => void;
}

// コンポーネントのメモ化
const ProductItem = memo(({ product, onAddToCart }: ProductItemProps) => {
  // コールバック関数のメモ化
  const handleAddToCart = useCallback(() => {
    onAddToCart(product.id);
  }, [product.id, onAddToCart]);

  // 計算結果のメモ化
  const discountedPrice = useMemo(() => {
    return product.price * (1 - product.discount);
  }, [product.price, product.discount]);

  return (
    <div className="product-item">
      <h3>{product.name}</h3>
      <p className="price">
        {product.discount > 0 && (
          <span className="original-price">¥{product.price}</span>
        )}
        <span className="final-price">¥{discountedPrice}</span>
      </p>
      <button onClick={handleAddToCart}>
        カートに追加
      </button>
    </div>
  );
});

export default ProductItem;

これらの具体例を参考に、段階的にReactからPreactへの移行を進めることができます。

図で理解できる要点

  • Next.jsではnext-plugin-preactを活用
  • CRAでは設定ファイルの調整が重要
  • 既存コンポーネントはHooksで書き換えるとより効率的

まとめ

移行判断のポイント

ReactからPreactへの移行を検討する際に重要な判断基準をまとめました。

1. プロジェクトの特性による判断

移行の適用シナリオを以下の表で整理いたします。

プロジェクト特性適用度理由
モバイルファースト★★★★★軽量化による高速化が顕著
静的サイト★★★★☆SSGとの相性が良好
複雑なSPA★★★☆☆ライブラリ互換性要確認
企業向けシステム★★☆☆☆安定性重視なら慎重に
プロトタイプ開発★★★★★高速開発が可能

2. 技術的要件の評価

移行前に確認すべき技術的ポイントです。

javascript// 移行適合性チェックリスト
const migrationReadinessCheck = {
  // 必須確認項目
  bundleSize: {
    current: '> 200KB', // 現在のバンドルサイズ
    threshold: '軽量化効果が期待できる'
  },
  dependencies: {
    reactSpecific: [], // React固有の依存関係
    compatible: []     // Preact互換ライブラリ
  },
  features: {
    ssr: false,        // SSR使用有無
    contextApi: false, // 高度なContext API使用
    experimental: false // 実験的機能使用
  },
  team: {
    experience: 'intermediate', // チームのスキルレベル
    bandwidth: 'medium'         // 移行作業の工数
  }
};

3. パフォーマンス向上の見込み

移行による具体的な改善効果を事前に試算しましょう。

javascript// パフォーマンス改善の試算例
const performanceEstimation = {
  bundleReduction: {
    before: '245KB (React bundle)',
    after: '15KB (Preact bundle)',
    improvement: '94% reduction'
  },
  loadTime: {
    '3G': { before: '3.2s', after: '0.8s', improvement: '75%' },
    '4G': { before: '1.1s', after: '0.3s', improvement: '73%' },
    'WiFi': { before: '0.4s', after: '0.1s', improvement: '75%' }
  },
  userExperience: {
    firstContentfulPaint: '50-70% faster',
    timeToInteractive: '60-80% faster',
    mobileScore: '+15-25 points'
  }
};

成功のコツ

Preact移行を成功させるための実践的なコツをご紹介します。

1. 段階的移行の実践

一度にすべてを変更するのではなく、段階的に進めることが重要です。

mermaidgantt
    title Preact移行スケジュール例
    dateFormat YYYY-MM-DD
    section 準備段階
    技術検証          :done,    prep1, 2024-01-01, 2024-01-07
    依存関係調査      :done,    prep2, 2024-01-08, 2024-01-14
    section 移行実装
    preact/compat導入 :active,  impl1, 2024-01-15, 2024-01-21
    設定ファイル調整  :         impl2, 2024-01-22, 2024-01-28
    コンポーネント移行 :         impl3, 2024-01-29, 2024-02-11
    section 検証・最適化
    テスト実行        :         test1, 2024-02-12, 2024-02-18
    パフォーマンス測定 :         perf1, 2024-02-19, 2024-02-25
    本番デプロイ      :         deploy, 2024-02-26, 2024-02-26

2. 品質保証の徹底

移行時の品質を保つための具体的なアプローチです。

typescript// テスト戦略の実装例
import { render, screen, fireEvent } from '@testing-library/preact';
import { expect, test, describe } from 'vitest';

describe('UserProfile Component', () => {
  test('ユーザー情報が正しく表示される', async () => {
    const mockUser = {
      id: 1,
      name: '田中太郎',
      email: 'tanaka@example.com'
    };

    // モックAPI設定
    global.fetch = jest.fn(() =>
      Promise.resolve({
        ok: true,
        json: () => Promise.resolve(mockUser)
      })
    );

    render(<UserProfile userId="1" />);
    
    // 非同期レンダリングの待機
    const userName = await screen.findByText('田中太郎');
    const userEmail = await screen.findByText('tanaka@example.com');
    
    expect(userName).toBeInTheDocument();
    expect(userEmail).toBeInTheDocument();
  });

  test('エラー状態が適切に処理される', async () => {
    // エラーシナリオのテスト
    global.fetch = jest.fn(() =>
      Promise.reject(new Error('ネットワークエラー'))
    );

    render(<UserProfile userId="1" />);
    
    const errorMessage = await screen.findByText(/エラー/);
    expect(errorMessage).toBeInTheDocument();
  });
});

3. パフォーマンス監視の継続

移行後も継続的にパフォーマンスを監視することが重要です。

javascript// パフォーマンス監視の実装
function setupPerformanceMonitoring() {
  // Core Web Vitals の測定
  new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      switch (entry.name) {
        case 'first-contentful-paint':
          console.log(`FCP: ${entry.startTime.toFixed(2)}ms`);
          break;
        case 'largest-contentful-paint':
          console.log(`LCP: ${entry.startTime.toFixed(2)}ms`);
          break;
      }
    }
  }).observe({ entryTypes: ['paint', 'largest-contentful-paint'] });

  // バンドルサイズ監視
  const bundleSize = performance.getEntriesByType('navigation')[0].transferSize;
  console.log(`Total bundle size: ${(bundleSize / 1024).toFixed(2)}KB`);
}

// アプリケーション起動時に実行
if (typeof window !== 'undefined') {
  setupPerformanceMonitoring();
}

4. チーム教育とドキュメント化

移行成功には、チーム全体の理解と協力が不可欠です。

markdown# Preact移行ガイド(チーム向け)

# 基本的な書き方の違い

## インポート文
```javascript
// Before (React)
import React, { useState } from 'react';

// After (Preact)
import { useState } from 'preact/hooks';
```

## コンポーネント定義
```javascript
// 推奨:関数コンポーネント + Hooks
function MyComponent({ title }) {
  const [count, setCount] = useState(0);
  return <div>{title}: {count}</div>;
}
```

# 注意点
1. preact/compatを使用する場合は従来通り
2. 新規コンポーネントは純粋なPreact書法を推奨
3. テストは@testing-library/preactを使用

5. 移行後の継続的改善

javascript// 継続的改善のためのメトリクス収集
const migrationMetrics = {
  // 技術メトリクス
  bundleSize: 'Monitor weekly',
  loadTime: 'Track daily',
  errorRate: 'Alert on increase',
  
  // ビジネスメトリクス
  userEngagement: 'Mobile users especially',
  conversionRate: 'Track improvement',
  bounceRate: 'Monitor reduction',
  
  // 開発メトリクス
  buildTime: 'Should be faster',
  deploymentFrequency: 'Maintain or improve',
  developerExperience: 'Regular team surveys'
};

ReactからPreactへの移行は、適切な計画と段階的な実行により、大幅なパフォーマンス向上をもたらします。特にモバイルユーザーにとっての体験改善は顕著で、ビジネス価値の向上にも直結します。

重要なのは、技術的な移行だけでなく、チーム全体での知識共有と継続的な改善サイクルを確立することです。本記事でご紹介した手法を参考に、ぜひPreactへの移行をご検討ください。

関連リンク