QR Send

スマホとPCのChrome間で、URLやテキストをQRコード1回のペアリングで送り合える自作Chrome拡張。アカウント不要・無料。

使ってみる →

Chrome拡張のMV3、個人開発者が本当にハマるのはservice worker・CSP・最小権限でした

個人開発

今月でMV2が選択肢から消えます

Chromeの公式ドキュメント(developer.chrome.com)によれば、Manifest V2の拡張機能は2026年8月31日にChromeウェブストアから完全に削除される予定だと案内されています。すでにChrome本体側では大半のユーザーでMV2拡張が無効化されており、今回はストア掲載そのものが終わる番です。「そのうちMV3に移行しよう」がついに通用しなくなる月ということになります。

私は自作のChrome拡張「QR Send」をMV3で開発し、Chromeウェブストアで公開しています。スマホから送ったURLやテキストをPCのタイムラインに表示する、という単純な拡張なのですが、それでもMV3特有のハマりどころには何度もぶつかりました。今回はその実体験をベースに、2026年8月時点で個人開発者が押さえておくべきポイントを、公式ドキュメントの裏取り付きで整理します。

service workerは寝る、そしてグローバル変数は消える

MV3ではbackground処理が「service worker」というイベント駆動の仕組みになっています。MV2までの常駐ページと違い、何もイベントが来ない状態が続くと自動的に終了(terminate)してしまいます。

Chrome公式のservice workerライフサイクルのドキュメントを確認したところ、通常は30秒間操作がないとservice workerが終了し、イベントやAPI呼び出しがあるとそのタイマーはリセットされると説明されています。加えて、1回のイベント処理が5分を超えて長引いた場合や、fetch()のレスポンス待ちが30秒を超えた場合にも終了する、とありました。この数値は年々見直されているので、正確な値を書くときは必ず現行の公式ページを確認するようにしています。

もう一つ見落としがちなのが、service workerが終了するとグローバル変数の中身がすべて消えることです。「動いているように見えるコード」がある日突然状態を失う、という壊れ方をするので、テスト中には気づきにくいバグの温床になります。

QR Sendで実際に踏んだ「ポップアップを開いても最新が出ない」問題

QR Sendはservice worker側でchrome.alarmsを使い、一定間隔でサーバーの新着をポーリングしています(このchrome.alarmsまわりの実装は別記事の「Chrome拡張 Manifest V3のservice workerはすぐ寝る:chrome.alarmsで定期処理を成立させる」で詳しく書きました)。ただ、これだけで満足していたら実際にユーザーから「スマホから送った文字列がPCタイムラインに出ない」という報告が来てしまいました。

原因は単純で、受信をアラーム任せにしていたせいで、ポップアップを開いた瞬間には最新が反映されていなかったのです。アラームの周期はChromeの仕様上どうしても最短30秒なので、運が悪いタイミングでポップアップを開くと、直近30秒以内に届いたメッセージが表示に間に合いません。

修正はポップアップ側のコードで、開いた瞬間に即時取得する処理を足すだけでした。

async function pollAndReload(showStatus = '') {
  try { await chrome.runtime.sendMessage({ type: 'poll-now' }); } catch (e) {}
  await loadHistory(showStatus);
}

async function init() {
  // ポップアップを開いた瞬間に最新を取得(アラーム待ちにしない)
  pollAndReload();
  // 開いている間は5秒ごとに自動更新(スマホから送った直後に反映される)
  refreshTimer = setInterval(() => pollAndReload(), 5000);
}

ポイントは2つです。ポップアップが開いたタイミングでservice worker宛てにpoll-nowメッセージを送って即時ポーリングさせること、そしてポップアップが表示されている間だけsetIntervalで5秒ごとに更新をかけ直すことです。ポップアップは開いている間はservice workerとは別に生きているページなので、ここでsetIntervalを使うこと自体は問題ありません(MV3で使えないのはservice worker側でのsetIntervalです)。ポップアップを閉じればこのタイマーも一緒に消えるので、無駄に動き続ける心配もありません。

アラームは「バックグラウンドで最低限の鮮度を保つ」役割に徹してもらい、「開いた瞬間の鮮度」はポップアップ側の責任にする、という役割分担にしたわけです。ここに気づくまで、アラームさえ組んでおけばリアルタイムに近い体験になるだろうと思い込んでいました。

状態を持たせたいならchrome.storage.session

service workerが終了するたびにグローバル変数が消えるという話をしましたが、では状態はどこに置けばいいのでしょうか。永続化したい値はchrome.storage.localで問題ありませんが、「拡張が動いている間だけ覚えておきたいが、ディスクには残したくない」というケースにはchrome.storage.sessionが用意されています。

公式ドキュメントによると、chrome.storage.sessionはメモリ上にデータを保持し、service workerのライフサイクルをまたいで値が残る一方、拡張の無効化・再読み込み・更新やブラウザの再起動でクリアされます。デフォルトではコンテンツスクリプトからは参照できず、必要ならsetAccessLevel()で明示的に開放する仕組みです。「消えてほしいけど、寝て起きるたびには消えてほしくない」という中間の要求にちょうど収まる場所だと感じました。

CSPで詰まるのはだいたい「外部から読み込むJS」

MV3ではCSP(コンテンツセキュリティポリシー)もかなり厳しくなりました。公式のセキュリティ改善ドキュメントを確認すると、MV3では拡張のロジックはすべてパッケージ内に含める必要があり、リモートでホストされたスクリプトを読み込んで実行することはできないと明記されています。

拡張ページに適用されるデフォルトのCSPはscript-src 'self'; object-src 'self';です。つまりインラインJavaScriptは実行されず、文字列を実行可能なコードとして評価することもできません。executeScript()eval()new Function()を使って外部のロジックを動かす、という手段はMV3では封じられています(サンドボックス化されたiframeの中でだけeval系は例外的に使えます)。

CDNからライブラリを読み込んでいたMV2時代のコードをそのまま持ってくると、ここで軒並み止まります。QR Sendでは外部ライブラリへの依存自体をやめて、必要な処理を自前のJSファイルとしてパッケージに同梱する方針に倒しました。地味ですが、これが一番手戻りが少ない対処法だと思います。

権限は「使う分だけ」、審査でも実際に聞かれる

MV3では権限の粒度も細かくなり、activeTaboptional_permissionsを使って「常に許可を求める」のではなく「必要になったときだけ許可を求める」設計が推奨されています。activeTabは今アクティブなタブに対する一時的な権限で、インストール時の警告表示もありません。ユーザーが操作した瞬間だけ使える権限、というイメージです。

QR SendのmanifestではSNS投稿のプレビュー機能のためにscripting権限と、Threads・X・Instagram・Bluesky限定のhost_permissionsを持たせています。

"permissions": [
  "alarms",
  "scripting",
  "storage",
  "tabs",
  "unlimitedStorage"
],
"host_permissions": [
  "https://threads.com/*",
  "https://x.com/*",
  "https://bsky.app/*",
  "https://www.instagram.com/*"
]

この権限を追加した途端、審査で説明を求められることになりました。「なぜこのホストへのアクセスが必要なのか」を具体的に書かないと通らない、という実体験は別記事の「Chrome拡張にscripting・host_permissionsを追加したら審査で何を聞かれるか(実体験)」にまとめてあります。ワイルドカードの<all_urls>を安易に使うと審査が長引くだけでなく、ユーザーからもインストール時に警告として見えてしまいます。個人開発の拡張なら、なおさら権限は機能に対して最小限に絞ったほうが得だと感じました。

まとめ

2026年8月時点でMV3の実践ポイントを3つに絞るなら、次のとおりです。

  • service workerは30秒前後で寝る前提でコードを書き、状態はグローバル変数ではなくchrome.storage.localchrome.storage.sessionに置く。「開いた瞬間の鮮度」が必要な画面は、アラーム任せにせず自分で即時取得する
  • CSPでリモートコードの実行は封じられている。外部ライブラリはパッケージに同梱するか、そもそも依存をやめる
  • 権限はactiveTaboptional_permissions・限定したhost_permissionsで必要な分だけ持たせる。審査でも実際に理由を聞かれるので、最初から説明できる範囲に絞っておいたほうが手戻りが少ない

MV2がいよいよストアから消える今月、まだMV3への移行が終わっていない拡張があれば、上の3点だけでも先に潰しておくと審査の往復が減ると思います。私自身、まだ全部の勘所を掴みきれているわけではないので、他にもハマりどころがあればコメントで教えてもらえると助かります。

コメント

タイトルとURLをコピーしました