🧩

Gemma 4 を使って Chrome 拡張機能を作ってみる

に公開

はじめに

2026年4月、Google から Gemma 4 が発表されました。軽量で、ローカルでも十分に機能するモデルとして公開されており、オンデバイス AI の選択肢として注目を集めています。

「軽いなら、ブラウザの中でも動かせるのでは?」と思い立って、Gemma 4 を使った Chrome 拡張機能を3つ作ってみたので軽く紹介します。

https://ai.google.dev/gemma/docs/core/model_card_4?hl=ja

実行環境

  • OS: Windows 11
  • CPU: Intel Core i7
  • memory: 32GB
  • GPU: GeForce RTX 2060

Gemma 4 をブラウザで動かす仕組み

全体像はシンプルです。

ポイントは Offscreen Document パターン です。Manifest V3 の service worker は WebGPU と DOM を使えないため、モデル本体を直接動かせません。そこで Chrome が提供する「非表示の DOM 環境」である Offscreen Document を使い、そこに transformers.js + WebGPU で Gemma 4 をロードします。

UI 層(ポップアップやサイドパネル)からは、ヘルパーが1つあれば呼べます。

const result = await llmClient.generate(messages, {
  max_new_tokens: 400,
  temperature: 0.5,
  onChunk: (text) => { /* ストリーミング受信 */ },
});

裏側で Port 接続 → service worker → offscreen document とメッセージが流れますが、UI 側のコードは generate() を呼ぶだけです。以降の拡張機能では、この1行が主役です。

作ってみた拡張機能3つ

① ページ要約

ツールバーのアイコンをクリックすると、開いているページを Gemma 4 が要約 してポップアップに表示します。ニュース記事やブログを開いて「要約する」を押すだけ。

実装の中心は、chrome.scripting.executeScript で対象タブから本文を抽出し、それをプロンプトとして Gemma 4 に渡す部分です。

const [{ result: pageText }] = await chrome.scripting.executeScript({
  target: { tabId: tab.id },
  func: extractPageText,  // article, main, [role=main] を優先的に拾う
});
await llmClient.generate([{ role: "user", content: "次のページを要約して…\n" + pageText }], {
  onChunk: (chunk) => { outputEl.textContent += chunk; },
});

試してみて気づいたのは、出力がとてつもなく遅い ことです。最近のクラウド LLM は数十〜数百 tokens/秒で応答するのが一般的ですが、私の環境の Gemma 4 は 5 tokens/秒程度 でした。長い出力をさせるのはなかなかきついので、max_new_tokens を抑え気味に設定するなどの工夫が必要です。また、出力の前に思考の過程が挟まるぶん、体感の待ち時間はさらに長くなります。とはいえ、動作自体は問題なくできていそうでした。

② 翻訳・言い換え

ページ上で選択したテキストを右クリックすると、「日本語に翻訳」「丁寧な表現に書き換え」 などのメニューが出ます。結果はフローティングパネルで表示され、気に入ったら「選択範囲を置換」で元のテキストを上書きできます。

仕組みは chrome.contextMenus でメニューを登録し、クリック時に content script へメッセージを飛ばすだけ。UI パネルは Shadow DOM で作ると、対象ページの CSS と干渉せずにきれいに差し込めます。

  • 翻訳: 英 → 日はほぼ違和感なし。専門用語の訳語ブレは Prompt でカバー
  • 書き換え: カジュアル → 丁寧語は得意。逆は少しクセが出る

③ 履歴付きサイドパネルチャット

Chrome のサイドパネルに常駐する Gemma 4 チャット です。履歴は chrome.storage.local に保存されるので、パネルを閉じても消えません。

Manifest V3 で追加された chrome.sidePanel API を使って、アイコンクリックで開閉できるようにしています。

// service-worker.js
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });

使ってみた感想としては、「タブを切り替えずに LLM と対話できる」体験はかなり快適です。ただ、チャットの実装を根気よく実装しないと、LLMチャット黎明期のコンテキスト度外視チャットアプリになるのでコンテキスト設計が十分に必要そうです。

やってみた所感

3つ作ってみて感じたことを正直に書きます。

良かったところ

  • ローカルで完結 のインパクトが大きい。個人情報を含む文章でも気兼ねなく LLM にかけられる
  • E2B で意外と実用的。翻訳・要約・短文チャットなら、クラウド LLM と遜色ない瞬間が多い
  • 2回目以降のロードは IndexedDB キャッシュから数秒なので、起動コストはあまり気にならない

気になったところ

  • 初回ダウンロードが約 500 MB: 一番軽量なモデルサイズが 500MB 程度であり、次に軽量なモデルが 1.5GB 程度です。拡張機能を作るなら機能を一つの拡張機能に多くの機能を盛り込むのもありかなと感じました。そうでないと、ストレージを圧迫する可能性がありそうです。
  • 生成速度は GPU 次第: 毎秒 5 トークンほどで、ノート PC の内蔵 GPU だとだいぶ遅く感じました
  • コンテキスト管理の実装: AI エージェント入りのチャットアプリを作ったことがある方ならわかると思いますが、コンテキストウィンドウ管理を実装するのが大変そうに感じました。ADK や LangChain などの AI エージェント向けライブラリには、スライディングウィンドウ方式や summarization 方式で履歴を管理できるモジュールがありますが、JavaScript にはそのような機能がないので自前で実装するしかなく、実装コストが高くなりそうです。

あと、Gemma 4 に限った話ですが、チャットテンプレート(<|turn> などの構造トークン)を自前で組み立てる必要があった のは少しハマりどころでした。tokenizer に chat_template が含まれていないパターンもあるので、モデルを差し替える際は公式ドキュメントで確認するのが安全です。

まとめ

Gemma 4 を Chrome 拡張機能に組み込んでみて感じたのは、「十分なローカル環境があればローカル LLM でもアイデア次第でちゃんと実用に届く」 と思いました。クラウド API を叩かなくても、要約・翻訳・チャットといった日常的なユースケースは十分に回ります。

一方で、生成速度・モデルサイズ・コンテキスト管理 の3点は正面から向き合う必要がある課題でもあります。「なんでもローカル LLM で済ませる」というよりは、プライバシーや API コストの観点でローカルが効く場面を見極めて組み込む、くらいが今のちょうど良い距離感かもしれません。

Discussion