ブラウザでAIを動かす時代が到来
2025年現在、AI開発のトレンドは「クラウド」から「エッジ」へと急速にシフトしています。特に注目されているのが、WebGPUを活用してブラウザ上で直接LLM(大規模言語モデル)を動かす技術です。
これには以下の革命的なメリットがあります:
- 完全なプライバシー: データがユーザーのデバイスから出ないため、機密情報を安全に扱えます。
- サーバーコストゼロ: 推論はユーザーのGPUで行われるため、高価なGPUサーバーを維持する必要がありません。
- 超低遅延: ネットワーク通信が発生しないため、オフラインでも動作し、応答が高速です。
本記事では、Transformers.js v3とReactを使用して、ブラウザだけで動作する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/transformers2. 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」を追加してみてください!
さらに理解を深める参考書
関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。


![[作って学ぶ]ブラウザのしくみ──HTTP、HTML、CSS、JavaScriptの裏側 (WEB+DB PRESS plusシリーズ)](https://m.media-amazon.com/images/I/41upB6FsPxL._SL500_.jpg)