T-CREATOR

3KB の軽量 React 代替 - Preact で始める高速 Web アプリ開発

3KB の軽量 React 代替 - Preact で始める高速 Web アプリ開発

Webアプリケーション開発において、パフォーマンスと開発効率のバランスは常に重要な課題です。 Reactは素晴らしいライブラリですが、ファイルサイズやパフォーマンスが気になる場面もあるでしょう。

そんな悩みを解決してくれるのがPreactです。 わずか3KBの軽量サイズでありながら、Reactとほぼ同じ機能と使い心地を提供してくれます。

Preactとは何か

PreactはJason Miller氏によって開発された、React互換のJavaScriptライブラリです。 2015年にリリースされて以来、軽量でありながら高機能なライブラリとして多くの開発者に愛用されています。

Preactは「最小限のReact」をコンセプトとして設計されており、Reactの主要機能をサポートしながらも大幅にサイズを削減することに成功しました。 その結果、モバイル環境や低帯域幅な環境でも快適に動作するWebアプリケーションを作成できます。

Preactが解決する問題

現代のWeb開発では、以下のような課題が存在します。

課題説明影響
1バンドルサイズの肥大化ページ読み込み速度の低下
2初期表示の遅延ユーザー体験の悪化
3モバイル環境での動作重量パフォーマンス問題
4学習コストの高さ開発効率の低下

Preactはこれらの課題を、軽量性と互換性の両立によって解決します。

ReactとPreactの違い

ReactとPreactには重要な違いがいくつか存在しますが、基本的な開発体験は非常に似ています。 主な違いを理解することで、プロジェクトに最適な選択ができるでしょう。

サイズの比較

最も顕著な違いはファイルサイズです。

ライブラリgzip圧縮後のサイズ備考
1React + ReactDOM約42KB
2Preact約3KB
3Preact/compat約4KB

この差は約14倍にも及び、特にモバイルアプリケーションやプログレッシブWebアプリ(PWA)において大きなアドバンテージとなります。

APIの違い

PreactはReactのコアAPIをほぼそのままサポートしていますが、いくつかの違いがあります。

typescript// React
import React from 'react';
import ReactDOM from 'react-dom';

// Preact
import { render } from 'preact';

主な違いは以下の通りです。

機能ReactPreact互換性
1classNameclasspreact/compat で互換
2React.FragmentFragment同等
3useEffectuseEffect同等
4useStateuseState同等

対応ブラウザ

PreactはIE11を含む幅広いブラウザをサポートしており、レガシー環境でも安心して使用できます。

Preactのメリット

Preactを採用することで得られるメリットは多岐にわたります。 特に以下の3つの観点から大きなメリットを享受できるでしょう。

ファイルサイズの軽量性

最大のメリットは圧倒的な軽量性です。

わずか3KBという軽量さは、ネットワーク転送時間を大幅に短縮し、初期表示速度を向上させます。 特にモバイル環境や帯域制限のある環境では、この差が顕著に現れるでしょう。

javascript// バンドルサイズの比較例
// React: vendor.js (200KB) + app.js (50KB) = 250KB
// Preact: vendor.js (50KB) + app.js (50KB) = 100KB
// 約60%のサイズ削減を実現

このサイズ削減により、以下の効果が期待できます。

  • ページ読み込み時間の短縮(約2-3倍高速化)
  • モバイルデータ通信量の削減
  • キャッシュ効率の向上

パフォーマンスの向上

軽量性は単にファイルサイズの問題だけではありません。 実行時のパフォーマンスも大幅に向上します。

PreactはReactよりも高速な仮想DOM実装を持っており、レンダリング処理が効率的に行われます。

javascript// パフォーマンス測定の例
const startTime = performance.now();
// Preact コンポーネントのレンダリング
render(<App />, document.body);
const endTime = performance.now();
console.log(`レンダリング時間: ${endTime - startTime}ms`);
// Reactと比較して20-30%高速

特に以下のような場面でパフォーマンスの違いが顕著に現れます。

シナリオパフォーマンス向上理由
1初期表示30-40%高速
2再レンダリング20-30%高速
3メモリ使用量40-50%削減

React互換性

PreactはReact互換性を重視して設計されており、既存のReactコードの多くをそのまま使用できます。

javascript// preact/compatを使用することで
// Reactライブラリとの互換性を確保
import { render } from 'preact/compat';
import { useState, useEffect } from 'preact/hooks';

// Reactと同じように記述可能
function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    document.title = `カウント: ${count}`;
  }, [count]);
  
  return (
    <div>
      <p>現在のカウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        増加
      </button>
    </div>
  );
}

この互換性により、以下のメリットを享受できます。

  • 既存のReactスキルをそのまま活用
  • 豊富なReactエコシステムの利用
  • 段階的な移行が可能

開発環境のセットアップ

Preactの開発環境をセットアップしましょう。 いくつかの方法がありますが、最も効率的な方法をご紹介します。

Preact CLIを使用したセットアップ

Preact CLIは公式が提供する開発ツールで、プロジェクトの初期化から本番ビルドまでを一貫してサポートします。

bash# Preact CLIのインストール
yarn global add preact-cli

# 新しいプロジェクトの作成
preact create my-preact-app
cd my-preact-app

この方法では、以下の設定が自動で完了します。

設定項目内容備考
1Webpack設定最適化済み
2TypeScript対応標準サポート
3PWA対応Service Worker
4Hot Reload開発効率化

手動でのセットアップ

既存プロジェクトにPreactを追加する場合は、手動でセットアップを行います。

bash# Preactのインストール
yarn add preact

# 開発用パッケージのインストール
yarn add -D @babel/core @babel/preset-env
yarn add -D webpack webpack-cli webpack-dev-server
yarn add -D babel-loader

Webpack設定ファイルを作成します。

javascript// webpack.config.js
module.exports = {
  entry: './src/index.js',
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            plugins: [
              ['@babel/plugin-transform-react-jsx', { pragma: 'h' }]
            ]
          }
        }
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.jsx'],
    alias: {
      'react': 'preact/compat',
      'react-dom': 'preact/compat'
    }
  },
  devServer: {
    contentBase: './dist',
    port: 3000,
    hot: true
  }
};

プロジェクト構造

推奨されるプロジェクト構造は以下の通りです。

csharpmy-preact-app/
├── src/
│   ├── components/       # 再利用可能なコンポーネント
│   ├── routes/          # ページコンポーネント
│   ├── assets/          # 静的ファイル
│   ├── style/           # スタイルシート
│   └── index.js         # エントリーポイント
├── public/
│   └── index.html       # HTMLテンプレート
└── package.json

基本的な使い方

Preactの基本的な使い方をマスターしましょう。 ReactとほぼD同じ感覚で開発できるため、学習コストは最小限に抑えられます。

コンポーネント作成

最初に、シンプルなコンポーネントから始めましょう。

javascript// src/components/Welcome.js
import { h } from 'preact';

// 関数コンポーネントの作成
function Welcome(props) {
  return (
    <div>
      <h1>こんにちは、{props.name}さん!</h1>
      <p>Preactへようこそ</p>
    </div>
  );
}

export default Welcome;

クラスコンポーネントも同様に作成できます。

javascript// src/components/Counter.js
import { Component } from 'preact';

class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  increment = () => {
    this.setState(prevState => ({
      count: prevState.count + 1
    }));
  }

  render() {
    return (
      <div>
        <p>カウント: {this.state.count}</p>
        <button onClick={this.increment}>
          クリック
        </button>
      </div>
    );
  }
}

export default Counter;

JSX記法

PreactではJSX記法をフルサポートしており、Reactと同じ感覚で記述できます。

javascript// JSXの基本的な使い方
import { h, render } from 'preact';
import { useState } from 'preact/hooks';

function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <div className={`todo-item ${todo.completed ? 'completed' : ''}`}>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span className="todo-text">{todo.text}</span>
      <button 
        className="delete-btn"
        onClick={() => onDelete(todo.id)}
      >
        削除
      </button>
    </div>
  );
}

イベントハンドリングも直感的に記述できます。

javascriptfunction InputForm({ onSubmit }) {
  const [inputValue, setInputValue] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (inputValue.trim()) {
      onSubmit(inputValue.trim());
      setInputValue('');
    }
  };

  return (
    <form onSubmit={handleSubmit} className="input-form">
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="新しいタスクを入力"
        className="task-input"
      />
      <button type="submit" className="submit-btn">
        追加
      </button>
    </form>
  );
}

propsとstate

propsとstateの概念はReactと全く同じです。

javascript// propsを受け取るコンポーネント
function UserProfile({ user, onEditClick }) {
  return (
    <div className="user-profile">
      <img 
        src={user.avatar} 
        alt={`${user.name}のアバター`}
        className="avatar"
      />
      <div className="user-info">
        <h2>{user.name}</h2>
        <p>{user.email}</p>
        <p>登録日: {user.registeredAt}</p>
        <button onClick={onEditClick} className="edit-btn">
          編集
        </button>
      </div>
    </div>
  );
}

Hooksを使用した状態管理も簡単です。

javascriptimport { useState, useEffect } from 'preact/hooks';

function UserSettings() {
  const [settings, setSettings] = useState({
    theme: 'light',
    notifications: true,
    language: 'ja'
  });

  const [isLoading, setIsLoading] = useState(true);

  // 設定の読み込み
  useEffect(() => {
    const loadSettings = async () => {
      try {
        const response = await fetch('/api/user/settings');
        const data = await response.json();
        setSettings(data);
      } catch (error) {
        console.error('設定の読み込みに失敗:', error);
      } finally {
        setIsLoading(false);
      }
    };

    loadSettings();
  }, []);

  const updateSetting = (key, value) => {
    setSettings(prev => ({
      ...prev,
      [key]: value
    }));
  };

  if (isLoading) {
    return <div className="loading">設定を読み込み中...</div>;
  }

  return (
    <div className="user-settings">
      <h2>ユーザー設定</h2>
      {/* 設定フォームの実装 */}
    </div>
  );
}

実践的な例:ToDoアプリの作成

実際にToDoアプリを作成して、Preactの機能を体験してみましょう。 この例では、状態管理、イベント処理、コンポーネント間のデータのやり取りを学びます。

アプリケーションの設計

まず、ToDoアプリに必要な機能を整理します。

機能説明実装方法
1タスク追加フォーム送信
2タスク削除ボタンクリック
3完了切り替えチェックボックス
4一覧表示リスト表示

メインコンポーネント

アプリケーション全体を管理するメインコンポーネントを作成します。

javascript// src/components/TodoApp.js
import { useState, useEffect } from 'preact/hooks';
import TodoList from './TodoList';
import TodoInput from './TodoInput';
import TodoFilter from './TodoFilter';

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all'); // all, active, completed
  const [nextId, setNextId] = useState(1);

  // ローカルストレージからデータを読み込み
  useEffect(() => {
    const savedTodos = localStorage.getItem('todos');
    if (savedTodos) {
      try {
        const parsedTodos = JSON.parse(savedTodos);
        setTodos(parsedTodos);
        
        // 次のIDを計算
        const maxId = Math.max(...parsedTodos.map(todo => todo.id), 0);
        setNextId(maxId + 1);
      } catch (error) {
        console.error('データの読み込みに失敗:', error);
      }
    }
  }, []);

  // todosが変更されたらローカルストレージに保存
  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  return (
    <div className="todo-app">
      <header className="app-header">
        <h1>My Todo App</h1>
        <p>Preactで作るシンプルなToDoアプリです</p>
      </header>
      
      <main className="app-main">
        <TodoInput onAddTodo={addTodo} />
        <TodoFilter 
          currentFilter={filter}
          onFilterChange={setFilter}
        />
        <TodoList 
          todos={filteredTodos}
          onToggleTodo={toggleTodo}
          onDeleteTodo={deleteTodo}
        />
      </main>
      
      <footer className="app-footer">
        <p>総タスク数: {todos.length} | 完了: {completedCount}</p>
      </footer>
    </div>
  );
}

タスク追加機能

新しいタスクを追加するためのコンポーネントを作成します。

javascript// src/components/TodoInput.js
import { useState } from 'preact/hooks';

function TodoInput({ onAddTodo }) {
  const [inputValue, setInputValue] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    // バリデーション
    if (!inputValue.trim()) {
      return;
    }

    if (inputValue.length > 100) {
      alert('タスクは100文字以内で入力してください');
      return;
    }

    setIsSubmitting(true);
    
    try {
      await onAddTodo(inputValue.trim());
      setInputValue('');
    } catch (error) {
      console.error('タスクの追加に失敗:', error);
      alert('タスクの追加に失敗しました');
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="todo-input-form">
      <div className="input-group">
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="新しいタスクを入力してください"
          className="task-input"
          disabled={isSubmitting}
          maxLength={100}
        />
        <button 
          type="submit" 
          className="add-btn"
          disabled={isSubmitting || !inputValue.trim()}
        >
          {isSubmitting ? '追加中...' : '追加'}
        </button>
      </div>
      <div className="input-info">
        <span className="char-count">
          {inputValue.length}/100文字
        </span>
      </div>
    </form>
  );
}

export default TodoInput;

タスク一覧表示

タスクの一覧を表示するコンポーネントを作成します。

javascript// src/components/TodoList.js
import TodoItem from './TodoItem';

function TodoList({ todos, onToggleTodo, onDeleteTodo }) {
  if (todos.length === 0) {
    return (
      <div className="empty-state">
        <p className="empty-message">
          まだタスクがありません
        </p>
        <p className="empty-hint">
          上のフォームから新しいタスクを追加してみましょう
        </p>
      </div>
    );
  }

  return (
    <div className="todo-list">
      <div className="list-header">
        <h2>タスク一覧</h2>
        <span className="task-count">{todos.length}件</span>
      </div>
      
      <ul className="todo-items">
        {todos.map(todo => (
          <li key={todo.id} className="todo-item-wrapper">
            <TodoItem
              todo={todo}
              onToggle={onToggleTodo}
              onDelete={onDeleteTodo}
            />
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoList;

個別タスクコンポーネント

各タスクを表示する詳細コンポーネントを作成します。

javascript// src/components/TodoItem.js
import { useState } from 'preact/hooks';

function TodoItem({ todo, onToggle, onDelete }) {
  const [isDeleting, setIsDeleting] = useState(false);

  const handleDelete = async () => {
    if (!confirm('このタスクを削除しますか?')) {
      return;
    }

    setIsDeleting(true);
    
    try {
      await onDelete(todo.id);
    } catch (error) {
      console.error('タスクの削除に失敗:', error);
      alert('タスクの削除に失敗しました');
      setIsDeleting(false);
    }
  };

  const formatDate = (timestamp) => {
    return new Date(timestamp).toLocaleString('ja-JP');
  };

  return (
    <div className={`todo-item ${todo.completed ? 'completed' : ''} ${isDeleting ? 'deleting' : ''}`}>
      <div className="todo-content">
        <label className="checkbox-wrapper">
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => onToggle(todo.id)}
            className="todo-checkbox"
          />
          <span className="checkmark"></span>
        </label>
        
        <div className="todo-text-content">
          <span className="todo-text">
            {todo.text}
          </span>
          <span className="todo-meta">
            作成日: {formatDate(todo.createdAt)}
            {todo.completedAt && (
              <span> | 完了日: {formatDate(todo.completedAt)}</span>
            )}
          </span>
        </div>
      </div>
      
      <div className="todo-actions">
        <button
          onClick={handleDelete}
          className="delete-btn"
          disabled={isDeleting}
        >
          {isDeleting ? '削除中...' : '削除'}
        </button>
      </div>
    </div>
  );
}

export default TodoItem;

状態管理の実装

ToDoアプリの状態管理ロジックを完成させます。

javascript// TodoApp.js の続き
const addTodo = (text) => {
  const newTodo = {
    id: nextId,
    text,
    completed: false,
    createdAt: Date.now(),
    completedAt: null
  };

  setTodos(prevTodos => [...prevTodos, newTodo]);
  setNextId(prev => prev + 1);
};

const toggleTodo = (id) => {
  setTodos(prevTodos =>
    prevTodos.map(todo =>
      todo.id === id
        ? {
            ...todo,
            completed: !todo.completed,
            completedAt: !todo.completed ? Date.now() : null
          }
        : todo
    )
  );
};

const deleteTodo = (id) => {
  setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
};

// フィルタリング機能
const filteredTodos = todos.filter(todo => {
  switch (filter) {
    case 'active':
      return !todo.completed;
    case 'completed':
      return todo.completed;
    default:
      return true;
  }
});

// 統計情報
const completedCount = todos.filter(todo => todo.completed).length;

ビルドと最適化

アプリケーションが完成したら、本番環境向けにビルドと最適化を行いましょう。 Preactは優れた最適化機能を提供しており、さらなるパフォーマンス向上が期待できます。

本番ビルドの実行

Preact CLIを使用している場合は、以下のコマンドでビルドを実行します。

bash# 本番ビルドの実行
yarn build

# ビルド結果の確認
yarn serve

手動設定の場合は、Webpack設定を本番モード用に調整します。

javascript// webpack.prod.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  mode: 'production',
  entry: './src/index.js',
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    publicPath: '/'
  },

  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', { targets: '> 0.25%, not dead' }]
            ],
            plugins: [
              ['@babel/plugin-transform-react-jsx', { pragma: 'h' }]
            ]
          }
        }
      }
    ]
  },

  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './public/index.html',
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeRedundantAttributes: true,
        useShortDoctype: true,
        removeEmptyAttributes: true,
        removeStyleLinkTypeAttributes: true,
        keepClosingSlash: true,
        minifyJS: true,
        minifyCSS: true,
        minifyURLs: true
      }
    })
  ],

  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
};

パフォーマンス分析

ビルド後のバンドルサイズを分析し、最適化の余地を確認します。

bash# webpack-bundle-analyzerのインストール
yarn add -D webpack-bundle-analyzer

# バンドル分析の実行
yarn webpack-bundle-analyzer dist/main.*.js

分析結果から以下の項目を確認できます。

項目確認ポイント最適化方法
1バンドルサイズ全体サイズ
2重複モジュール同じライブラリの複数読み込み
3未使用コードインポートされているが未使用
4大きなライブラリサイズの大きなライブラリ

最適化テクニック

さらなる最適化のためのテクニックをご紹介します。

javascript// 動的インポートによるコード分割
import { lazy, Suspense } from 'preact/compat';

// 重いコンポーネントを遅延読み込み
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <div>
      <Suspense fallback={<div>読み込み中...</div>}>
        <HeavyComponent />
      </Suspense>
    </div>
  );
}

メモ化によるレンダリング最適化も重要です。

javascriptimport { memo } from 'preact/compat';
import { useMemo, useCallback } from 'preact/hooks';

// メモ化されたコンポーネント
const TodoItem = memo(({ todo, onToggle, onDelete }) => {
  // 高価な計算をメモ化
  const formattedDate = useMemo(() => {
    return new Date(todo.createdAt).toLocaleDateString('ja-JP');
  }, [todo.createdAt]);

  // コールバック関数をメモ化
  const handleToggle = useCallback(() => {
    onToggle(todo.id);
  }, [todo.id, onToggle]);

  return (
    <div className="todo-item">
      {/* コンポーネントの実装 */}
    </div>
  );
});

まとめ

Preactは軽量でありながら、Reactと同等の機能性を提供する優れたライブラリです。 わずか3KBという驚異的な軽量さにより、パフォーマンスを重視するWebアプリケーション開発において大きなアドバンテージを得られるでしょう。

今回学んだ内容をまとめると以下の通りです。

Preactの主要なメリット

  • 圧倒的な軽量性(3KB)によるパフォーマンス向上
  • Reactとの高い互換性による学習コストの低減
  • 優秀な開発者エクスペリエンスとツールサポート

実践で活用できる知識

  • 基本的なコンポーネント作成とJSX記法
  • Hooks を活用した現代的な状態管理
  • 実用的なアプリケーション開発手法

最適化とパフォーマンス

  • 効率的なビルド設定とバンドル最適化
  • コード分割とメモ化による高速化
  • 本番環境での運用ノウハウ

Preactを採用することで、高速で軽量なWebアプリケーションを効率的に開発できるようになります。 特にモバイルファーストな現代のWeb開発において、その価値は非常に高いと言えるでしょう。

ぜひ今回学んだ内容を活用して、素晴らしいWebアプリケーションを作成してみてください。 Preactの軽量性と柔軟性が、あなたの開発体験をより良いものにしてくれるはずです。

関連リンク