Tasuke HubLearn · Solve · Grow
#WebGPU

WebGPUで動くブラウザ完結LLM実装ガイド【2025年最新】

Transformers.js v3とWebGPUを使って、サーバー不要・完全プライベートなAIチャットアプリをReactで構築する方法を解説します。

時計のアイコン26 November, 2025
TH

Tasuke Hub管理人

東証プライム市場上場企業エンジニア

情報系修士卒業後、大手IT企業にてフルスタックエンジニアとして活躍。 Webアプリケーション開発からクラウドインフラ構築まで幅広い技術に精通し、 複数のプロジェクトでリードエンジニアを担当。 技術ブログやオープンソースへの貢献を通じて、日本のIT技術コミュニティに積極的に関わっている。

🎓情報系修士🏢東証プライム上場企業💻フルスタックエンジニア📝技術ブログ執筆者

ブラウザでAIを動かす時代が到来

2025年現在、AI開発のトレンドは「クラウド」から「エッジ」へと急速にシフトしています。特に注目されているのが、WebGPUを活用してブラウザ上で直接LLM(大規模言語モデル)を動かす技術です。

これには以下の革命的なメリットがあります:

  1. 完全なプライバシー: データがユーザーのデバイスから出ないため、機密情報を安全に扱えます。
  2. サーバーコストゼロ: 推論はユーザーのGPUで行われるため、高価なGPUサーバーを維持する必要がありません。
  3. 超低遅延: ネットワーク通信が発生しないため、オフラインでも動作し、応答が高速です。

本記事では、Transformers.js v3Reactを使用して、ブラウザだけで動作するAIチャットアプリを実装する方法を解説します。

ベストマッチ

最短で課題解決する一冊

この記事の内容と高い親和性が確認できたベストマッチです。早めにチェックしておきましょう。

前提条件

  • ブラウザ: Chrome 113以降、Edge、またはWebGPUをサポートするその他のブラウザ
  • ハードウェア: WebGPU対応のGPU(最近のPCやMacならほぼ対応しています)
  • 知識: Reactの基礎知識

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

実装ステップ

今回は、UIスレッドをブロックしないように、重い推論処理をWeb Workerで実行する設計にします。

1. プロジェクトのセットアップ

まずはViteでReactプロジェクトを作成し、必要なライブラリをインストールします。

npm create vite@latest browser-llm-chat -- --template react
cd browser-llm-chat
npm install @huggingface/transformers

2. Web Workerの実装 (worker.js)

public/worker.js(またはsrc内に配置して適切にバンドル)を作成します。ここでは簡単のためsrc/worker.jsとし、ViteのWorkerインポート機能を使用します。

このWorkerは、モデルのロードと推論を担当します。

// src/worker.js
import { pipeline, env } from '@huggingface/transformers';

// 初回ロード時のチェックをスキップ(任意)
env.allowLocalModels = false;
env.useBrowserCache = true;

class AIWorker {
  static instance = null;

  static async getInstance(progress_callback = null) {
    if (this.instance === null) {
      // テキスト生成タスクのパイプラインを作成
      // WebGPUを有効にするには device: 'webgpu' を指定
      // モデルは軽量な 'Xenova/LaMini-Flan-T5-248M' や 'Xenova/Qwen1.5-0.5B-Chat' などがおすすめ
      this.instance = await pipeline('text-generation', 'Xenova/Qwen1.5-0.5B-Chat', {
        device: 'webgpu',
        dtype: 'q4', // 量子化モデルを使用してメモリを節約
        progress_callback,
      });
    }
    return this.instance;
  }
}

// メインスレッドからのメッセージを受信
self.addEventListener('message', async (event) => {
  const { type, data } = event.data;

  if (type === 'generate') {
    try {
      const generator = await AIWorker.getInstance((progress) => {
        // モデルのロード進捗を通知
        self.postMessage({ type: 'progress', data: progress });
      });

      const messages = data.messages; // チャット履歴

      // 推論実行
      const output = await generator(messages, {
        max_new_tokens: 128,
        temperature: 0.7,
        do_sample: true,
        // ストリーミングが必要な場合はcallbackを使用(今回は簡易化のため一括返却)
      });

      // 結果を送信
      self.postMessage({ type: 'complete', data: output });
    } catch (error) {
      self.postMessage({ type: 'error', data: error.message });
    }
  }
});

3. Reactコンポーネントの実装 (App.jsx)

次に、Workerと通信するチャットUIを作成します。

// src/App.jsx
import { useState, useEffect, useRef } from 'react';

function App() {
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [progress, setProgress] = useState(null);
  const worker = useRef(null);

  useEffect(() => {
    // Workerの初期化
    worker.current = new Worker(new URL('./worker.js', import.meta.url), {
      type: 'module',
    });

    // Workerからのメッセージハンドリング
    worker.current.onmessage = (event) => {
      const { type, data } = event.data;

      if (type === 'progress') {
        // モデルロードの進捗表示
        if (data.status === 'progress') {
          setProgress(`モデルをロード中... ${Math.ceil(data.progress)}%`);
        } else if (data.status === 'ready') {
          setProgress(null);
        }
      } else if (type === 'complete') {
        // 生成完了
        const generatedText = data[0].generated_text;
        // QwenなどのChatモデルは会話履歴全体のリストを返すことがあるため、最後の応答を抽出
        const lastMessage = generatedText[generatedText.length - 1];
        
        setMessages((prev) => [...prev, lastMessage]);
        setIsLoading(false);
      } else if (type === 'error') {
        console.error('AI Error:', data);
        setIsLoading(false);
        alert('エラーが発生しました。コンソールを確認してください。');
      }
    };

    return () => {
      worker.current.terminate();
    };
  }, []);

  const sendMessage = () => {
    if (!input.trim() || isLoading) return;

    const userMessage = { role: 'user', content: input };
    const newMessages = [...messages, userMessage];
    
    setMessages(newMessages);
    setInput('');
    setIsLoading(true);

    // Workerに推論リクエストを送信
    worker.current.postMessage({
      type: 'generate',
      data: { messages: newMessages },
    });
  };

  return (
    <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
      <h1>WebGPU AI Chat</h1>
      
      {/* 進捗表示 */}
      {progress && (
        <div style={{ padding: '10px', background: '#e0f7fa', borderRadius: '5px', marginBottom: '10px' }}>
          {progress}
        </div>
      )}

      {/* チャットエリア */}
      <div style={{ 
        height: '400px', 
        overflowY: 'auto', 
        border: '1px solid #ccc', 
        borderRadius: '8px',
        padding: '10px',
        marginBottom: '20px',
        display: 'flex',
        flexDirection: 'column',
        gap: '10px'
      }}>
        {messages.map((msg, index) => (
          <div key={index} style={{
            alignSelf: msg.role === 'user' ? 'flex-end' : 'flex-start',
            background: msg.role === 'user' ? '#007bff' : '#f1f1f1',
            color: msg.role === 'user' ? 'white' : 'black',
            padding: '10px',
            borderRadius: '10px',
            maxWidth: '80%'
          }}>
            <strong>{msg.role === 'user' ? 'あなた' : 'AI'}:</strong>
            <p style={{ margin: '5px 0 0' }}>{msg.content}</p>
          </div>
        ))}
        {isLoading && <div style={{ alignSelf: 'flex-start', color: '#888' }}>AIが入力中...</div>}
      </div>

      {/* 入力エリア */}
      <div style={{ display: 'flex', gap: '10px' }}>
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
          placeholder="メッセージを入力..."
          style={{ flex: 1, padding: '10px', borderRadius: '5px', border: '1px solid #ddd' }}
          disabled={isLoading}
        />
        <button 
          onClick={sendMessage} 
          disabled={isLoading || !!progress}
          style={{ padding: '10px 20px', background: '#007bff', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}
        >
          送信
        </button>
      </div>
    </div>
  );
}

export default App;

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

パフォーマンス最適化のポイント

実運用レベルのアプリにするための重要なポイントです。

1. モデルの量子化 (Quantization)

上記のコードで dtype: 'q4' を指定しています。これは4ビット量子化を意味し、モデルサイズを劇的に(約1/4に)削減します。ブラウザでの実行にはほぼ必須の設定です。

2. キャッシュの活用

Transformers.jsはデフォルトでCache APIを使用します。2回目以降のアクセスではモデルのダウンロードがスキップされるため、起動が高速になります。

3. WebGPUの互換性チェック

WebGPUが利用できない環境(古いスマホなど)のために、WASMへのフォールバックを検討するか、適切なエラーメッセージを表示しましょう。

if (!navigator.gpu) {
  alert("WebGPUがサポートされていません。Chromeの最新版をご利用ください。");
}

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

まとめ

WebGPUとTransformers.jsの進化により、フロントエンドエンジニアだけでも高度なAIアプリが作れるようになりました。 サーバー代を気にせず、ユーザーのプライバシーを守れるこのアーキテクチャは、2025年の個人開発や社内ツール開発において強力な選択肢となるでしょう。

ぜひ、あなたのポートフォリオに「ブラウザ完結AI」を追加してみてください!

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

この記事をシェア

続けて読みたい記事

編集部がピックアップした関連記事で学びを広げましょう。

#Python

PythonだけでモダンなWebアプリが作れる!Reflex入門ガイド【2025年最新】

2025/11/26
#Next.js

Next.jsとTypeScriptでAI統合Webアプリを構築する完全ガイド【2025年最新】

2025/8/12
#LLM

LLMアプリ評価指標と実装ガイド【2025年版】:自動評価・人手評価・オンライン評価の設計

2025/9/13
#AI

AIガバナンス・プラットフォーム実装ガイド - Python・MLOps完全版【2025年最新】

2025/8/14
#ベクターデータベース

ベクターデータベースで構築するセマンティック検索システム完全ガイド【2025年最新】

2025/8/12
#AI

【2025年最新】LLMファインチューニング効率化ガイド:コスト削減と精度向上を両立する実践テクニック

2025/11/27