PaPoo
cover
technews
Author
technews
世界の技術ニュースをリアルタイムでキャッチし、日本語でわかりやすく発信。AI・半導体・スタートアップから規制動向まで、グローバルテックシーンの「今」をお届けします。

MV3 Service Worker時代にGoogle Drive同期エンジンを作るには何を捨てるべきか

記事のキーポイント

本文

Stack Overflowの記事「Building a Google Drive Sync Engine that Survives MV3 Service Workers」は、Chrome拡張の世界で起きている地味だけどかなり大きな変化を、実例ベースで説明しています。
ひとことで言うと、​MV3になったことで、拡張機能の作り方をかなり根本から見直さないといけなくなった、という話です。

一般の人から見ると「ブラウザ拡張の内部事情なんて関係あるの?」と思うかもしれません。
でも実はこれ、かなり本質的です。なぜなら、ブラウザ拡張は“ちょっと便利な小物”に見えて、裏ではちゃんと同期、保存、通信、オフライン対応をやっているからです。そこが壊れると、見た目が地味でも体験は一気に崩れます。

まず何が変わったのか: MV3は“常駐”を許してくれない

記事の中心にあるのは、ChromeのManifest V3(MV3)です。
MV3では、拡張機能のバックグラウンド処理を担うService Workerが、​必要なときだけ起きて、仕事が終わればすぐ眠るような動きになります。

これ、サーバー的な感覚で考えるとかなり厄介です。
昔のMV2では、バックグラウンドのJavaScript変数に同期キューを持っておいて、「あとでまとめて送る」みたいな作りができました。
でもMV3では、​メモリに置いた状態は信用できない。ブラウザがメモリ節約のためにService Workerを落とすことがあるからです。

記事の筆者はここをかなり強く言っています。
もしユーザーがWebページをクリップして、アップロードが終わる前にService Workerが死んだら、そのデータは消える。これは怖い。かなり怖いです。
個人的には、ここがMV3のいちばん“見た目以上に重い”ポイントだと思います。

解決策は「disk-first」: 正本はローカル保存に置く

そこで筆者が採った方針が、​disk-first model です。
つまり、まずローカルに保存する。同期は後から考える。順番を逆にするわけです。

ここで使うのが chrome.storage.local
これは拡張機能が使えるローカル保存領域で、雑に言えば「ブラウザ内の小さなデータ保管庫」です。
Service Workerのメモリと違って、ブラウザが落ちてもデータは残るので、同期の“本体”としてはこちらのほうがずっと信頼できます。

記事では、ユーザーのアクション――たとえば、

といった操作が起きたら、​すぐにlocal storageへ保存するようにしていると説明しています。
クラウド同期はあくまで“後からついてくる処理”。この考え方はとても現実的です。
正直、キラキラした設計ではないけれど、壊れにくい。こういう地味な設計のほうが、結局はユーザーにとってありがたいんですよね。

オフラインは前提。問題は「戻ってきたとき」

次の論点はオフラインです。
ブラウザ拡張がネットワーク不安定な環境で動く以上、​通信が切れることは例外ではなく日常です。

筆者の設計では、オフラインになったら同期を止めて、ローカルにキューしておく。ここまではまあ想像がつきます。
本当に難しいのは、​オンラインに戻った瞬間に何をどう送るかです。

ここで雑に「ローカルの変更を全部クラウドに上書き」すると危険です。
たとえば、別のPCからユーザーが同じデータを更新していたら、古いローカル内容で新しい変更を上書きしてしまうかもしれない。
こういう事故は、クラウド同期の世界ではあるあるです。だからこそ、単純な上書きは危ない。

Google Drive上のJSONを読み、手でマージする

筆者は、Google Driveの appDataFolder に置いたJSONを読み込み、それとローカルのメモをマージしています。
マージというのは、複数のデータを突き合わせて、ひとつの正しい結果にまとめることです。

やり方としては、

  1. Driveから既存のJSONを取得する
  2. ローカルのメモと一緒に Map に入れる
  3. 重複や順序を整理する
  4. ひとつの配列にまとめて再アップロードする

という流れです。

ここで面白いのは、筆者のnote IDがほぼタイムスタンプなので、​時系列で並べるのが簡単だという点です。
つまり「いつ作られたメモか」がIDからわかるので、重複排除や並び替えがやりやすい。
これは設計としてかなり気持ちがいいです。最初から“同期しやすいID”を考えておくと、あとで地獄を見にくいんですよね。

もちろん、記事中でも「ちょっとhacky」と言っています。
でも、hackyでも事故を防げるなら、それは十分に価値がある。ここは実務っぽくて好きなところです。
理想のアルゴリズムより、​壊れない現実解のほうがずっと偉い、という場面は本当に多いです。

Google SDKを捨てて native fetch にする決断

記事でもうひとつ大きい決断として語られるのが、​Googleの公式API client SDKを使わず、native fetchでDrive APIを叩くことです。

SDKは便利です。これは間違いない。
認証やAPI呼び出しをかなり楽にしてくれます。
でもその代わり、依存関係が重い。MV3のService Workerでは、​できるだけ軽く、速く、起動が速いことが重要なので、巨大なSDKは邪魔になりやすいわけです。

筆者はここで、性能とシンプルさを優先してSDKを外しました。
この判断はかなりMV3らしいです。
「便利さを買うか、起動速度を買うか」。で、今回は後者を選んだ、という感じですね。

個人的には、この判断はかなり筋が通っていると思います。
ブラウザ拡張は“常に動き続けるアプリ”ではないので、巨大なライブラリを積むと、起動のたびにちょっとずつ不利が積み重なります。
拡張機能の世界では、その“ちょっとずつ”が体感差になるんですよね。

ただし代償はある: multipart/related を手で組む

もちろん、便利なSDKを捨てれば、その分だけ面倒が増えます。
筆者が挙げている最大の面倒は、​multipart/related HTTP bodyを手作業で組み立てることです。

これは何かというと、メタデータとファイル内容をまとめて1回のリクエストで送るための、ちょっと複雑なHTTP形式です。
普段はSDKが全部やってくれるのですが、native fetchでやるなら、自分で

といった細かい作業をしないといけません。

記事中のコードでは、boundary を作って、delimiterclose_delim を組み立てて、最終的に文字列をつなげています。
これ、率直に言ってかなり地味で、しかもミスりやすい。
改行ひとつ違うだけで壊れる世界なので、精神的にはあまり優しくないです。

でも、筆者はそれでも「軽くて速いからやる価値がある」と言っています。
ここは完全にトレードオフですね。
書くのは面倒、でも動けば気持ちいい。こういう実装、エンジニアはつい好きになってしまうんだと思います。

記事が伝えたい本当のメッセージ

最後に筆者は、MV3を「制約だらけ」と感じるのではなく、​制約を前提に設計することが大事だと言っています。

これはかなり重要な視点です。
制約って、普通はうっとうしいです。できれば避けたい。
でも、制約があるからこそ、設計が締まることもあります。

この記事の流れをまとめると、筆者は次のような考え方にたどり着いています。

この発想は、ブラウザ拡張に限らず、オフラインファーストなアプリ全般に通じると思います。
そして何より、​​「理想のアーキテクチャ」より「制約の中で生き残る設計」​のほうが実戦では強い、ということを思い出させてくれます。

まとめ: MV3は不便だけど、設計を鍛える

この記事は、Chrome拡張のMV3対応をただの移行作業としてではなく、​設計思想の見直しとして描いています。
Service Workerは落ちる。ネットワークは切れる。SDKは重い。
その現実を認めたうえで、ローカル保存中心・オフライン耐性・軽量依存という方向に舵を切る。

正直、面倒です。かなり面倒。
でも、その面倒さを乗り越えると、ブラウザにちゃんと馴染む、壊れにくい同期エンジンができる。
この記事は、そのことをかなり実感のこもった形で教えてくれます。

私はこういう「仕様変更で嫌でも設計が鍛えられる話」が好きです。
楽ではないけど、学びが深い。しかも、実際に動くものを作る人の苦労が見えるので、読んでいて面白い。
MV3はたしかに厳しいですが、厳しいからこそ、設計の腕前が出る世界なのだと思います。


参考: Building a Google Drive Sync Engine that Survives MV3 Service Workers - Stack Overflow

同じ著者の記事