Stack Overflowの記事「Building a Google Drive Sync Engine that Survives MV3 Service Workers」は、Chrome拡張の世界で起きている地味だけどかなり大きな変化を、実例ベースで説明しています。
ひとことで言うと、MV3になったことで、拡張機能の作り方をかなり根本から見直さないといけなくなった、という話です。
一般の人から見ると「ブラウザ拡張の内部事情なんて関係あるの?」と思うかもしれません。
でも実はこれ、かなり本質的です。なぜなら、ブラウザ拡張は“ちょっと便利な小物”に見えて、裏ではちゃんと同期、保存、通信、オフライン対応をやっているからです。そこが壊れると、見た目が地味でも体験は一気に崩れます。
記事の中心にあるのは、ChromeのManifest V3(MV3)です。
MV3では、拡張機能のバックグラウンド処理を担うService Workerが、必要なときだけ起きて、仕事が終わればすぐ眠るような動きになります。
これ、サーバー的な感覚で考えるとかなり厄介です。
昔のMV2では、バックグラウンドのJavaScript変数に同期キューを持っておいて、「あとでまとめて送る」みたいな作りができました。
でもMV3では、メモリに置いた状態は信用できない。ブラウザがメモリ節約のためにService Workerを落とすことがあるからです。
記事の筆者はここをかなり強く言っています。
もしユーザーがWebページをクリップして、アップロードが終わる前にService Workerが死んだら、そのデータは消える。これは怖い。かなり怖いです。
個人的には、ここがMV3のいちばん“見た目以上に重い”ポイントだと思います。
そこで筆者が採った方針が、disk-first model です。
つまり、まずローカルに保存する。同期は後から考える。順番を逆にするわけです。
ここで使うのが chrome.storage.local。
これは拡張機能が使えるローカル保存領域で、雑に言えば「ブラウザ内の小さなデータ保管庫」です。
Service Workerのメモリと違って、ブラウザが落ちてもデータは残るので、同期の“本体”としてはこちらのほうがずっと信頼できます。
記事では、ユーザーのアクション――たとえば、
といった操作が起きたら、すぐにlocal storageへ保存するようにしていると説明しています。
クラウド同期はあくまで“後からついてくる処理”。この考え方はとても現実的です。
正直、キラキラした設計ではないけれど、壊れにくい。こういう地味な設計のほうが、結局はユーザーにとってありがたいんですよね。
次の論点はオフラインです。
ブラウザ拡張がネットワーク不安定な環境で動く以上、通信が切れることは例外ではなく日常です。
筆者の設計では、オフラインになったら同期を止めて、ローカルにキューしておく。ここまではまあ想像がつきます。
本当に難しいのは、オンラインに戻った瞬間に何をどう送るかです。
ここで雑に「ローカルの変更を全部クラウドに上書き」すると危険です。
たとえば、別のPCからユーザーが同じデータを更新していたら、古いローカル内容で新しい変更を上書きしてしまうかもしれない。
こういう事故は、クラウド同期の世界ではあるあるです。だからこそ、単純な上書きは危ない。
筆者は、Google Driveの appDataFolder に置いたJSONを読み込み、それとローカルのメモをマージしています。
マージというのは、複数のデータを突き合わせて、ひとつの正しい結果にまとめることです。
やり方としては、
という流れです。
ここで面白いのは、筆者のnote IDがほぼタイムスタンプなので、時系列で並べるのが簡単だという点です。
つまり「いつ作られたメモか」がIDからわかるので、重複排除や並び替えがやりやすい。
これは設計としてかなり気持ちがいいです。最初から“同期しやすいID”を考えておくと、あとで地獄を見にくいんですよね。
もちろん、記事中でも「ちょっとhacky」と言っています。
でも、hackyでも事故を防げるなら、それは十分に価値がある。ここは実務っぽくて好きなところです。
理想のアルゴリズムより、壊れない現実解のほうがずっと偉い、という場面は本当に多いです。
記事でもうひとつ大きい決断として語られるのが、Googleの公式API client SDKを使わず、native fetchでDrive APIを叩くことです。
SDKは便利です。これは間違いない。
認証やAPI呼び出しをかなり楽にしてくれます。
でもその代わり、依存関係が重い。MV3のService Workerでは、できるだけ軽く、速く、起動が速いことが重要なので、巨大なSDKは邪魔になりやすいわけです。
筆者はここで、性能とシンプルさを優先してSDKを外しました。
この判断はかなりMV3らしいです。
「便利さを買うか、起動速度を買うか」。で、今回は後者を選んだ、という感じですね。
個人的には、この判断はかなり筋が通っていると思います。
ブラウザ拡張は“常に動き続けるアプリ”ではないので、巨大なライブラリを積むと、起動のたびにちょっとずつ不利が積み重なります。
拡張機能の世界では、その“ちょっとずつ”が体感差になるんですよね。
もちろん、便利なSDKを捨てれば、その分だけ面倒が増えます。
筆者が挙げている最大の面倒は、multipart/related HTTP bodyを手作業で組み立てることです。
これは何かというと、メタデータとファイル内容をまとめて1回のリクエストで送るための、ちょっと複雑なHTTP形式です。
普段はSDKが全部やってくれるのですが、native fetchでやるなら、自分で
といった細かい作業をしないといけません。
記事中のコードでは、boundary を作って、delimiter と close_delim を組み立てて、最終的に文字列をつなげています。
これ、率直に言ってかなり地味で、しかもミスりやすい。
改行ひとつ違うだけで壊れる世界なので、精神的にはあまり優しくないです。
でも、筆者はそれでも「軽くて速いからやる価値がある」と言っています。
ここは完全にトレードオフですね。
書くのは面倒、でも動けば気持ちいい。こういう実装、エンジニアはつい好きになってしまうんだと思います。
最後に筆者は、MV3を「制約だらけ」と感じるのではなく、制約を前提に設計することが大事だと言っています。
これはかなり重要な視点です。
制約って、普通はうっとうしいです。できれば避けたい。
でも、制約があるからこそ、設計が締まることもあります。
この記事の流れをまとめると、筆者は次のような考え方にたどり着いています。
この発想は、ブラウザ拡張に限らず、オフラインファーストなアプリ全般に通じると思います。
そして何より、「理想のアーキテクチャ」より「制約の中で生き残る設計」のほうが実戦では強い、ということを思い出させてくれます。
この記事は、Chrome拡張のMV3対応をただの移行作業としてではなく、設計思想の見直しとして描いています。
Service Workerは落ちる。ネットワークは切れる。SDKは重い。
その現実を認めたうえで、ローカル保存中心・オフライン耐性・軽量依存という方向に舵を切る。
正直、面倒です。かなり面倒。
でも、その面倒さを乗り越えると、ブラウザにちゃんと馴染む、壊れにくい同期エンジンができる。
この記事は、そのことをかなり実感のこもった形で教えてくれます。
私はこういう「仕様変更で嫌でも設計が鍛えられる話」が好きです。
楽ではないけど、学びが深い。しかも、実際に動くものを作る人の苦労が見えるので、読んでいて面白い。
MV3はたしかに厳しいですが、厳しいからこそ、設計の腕前が出る世界なのだと思います。
参考: Building a Google Drive Sync Engine that Survives MV3 Service Workers - Stack Overflow