
これまで、WebCatalogデスクトップアプリで作成するアプリは、macOS上で1つあたり約320 MBのディスク容量を使用していました。Electronベースのアプリとしては珍しくありませんが、WebCatalogは多くのWebアプリをデスクトップアプリとして使う人向けに設計されています。10個インストールすれば、内部のデータの大半がまったく同じでも、3 GB以上を消費する可能性がありました。
最近、WebCatalogデスクトップアプリがmacOS上でアプリをビルドする方法を変更しました。最初のアプリは、ローカルに保持する準備済みのアプリエンジンのコピーを含めて、約340 MBの物理ディスク容量を使用します。その後は、アプリを1つ追加するごとに増える実際のディスク使用量は、通常わずか1~2 MBです。テストでは、10個のアプリの使用量が3 GB以上から約360 MBになりました。
これには、macOSに備わっているAPFSクローンという機能を使いました。難しかったのは、単にファイルをクローンすることではありません。各アプリの独立性を保ち、コード署名を正しく行い、従来の方法でビルドしたアプリと同じように動作させながら、クローンを機能させることでした。
なぜアプリごとに320 MBも増えていたのか
WebCatalogデスクトップアプリは、Webサイトを独立したデスクトップアプリに変換します。作成した各アプリは、Electronを基盤とする当社のアプリエンジンPhoton上で動作します。macOSでは、各アプリはElectron(ChromiumとNode.js)、Photon、そのアプリ固有のファイルを含む通常の.appバンドルです。
たとえばSlackとDiscordの2つのアプリを考えてみましょう。名前、アイコン、バンドル識別子、設定は異なりますが、容量の大きいElectronフレームワークとPhotonの大部分は同一です。
これまでは、各アプリを完全に別々のコピーとしてビルドしていました。
Slack.app
Electron
Photon
Slack固有のファイル
Discord.app
Electron
Photon
Discord固有のファイル
両アプリのElectronフレームワークとPhotonがバイト単位で同一でも、macOSはアプリごとに別の物理コピーを保存していました。そのため、10個のアプリは、ほぼ同じ320 MBのアプリケーションバンドルを約10個保存することを意味していました。
一方、この構成には維持したい特性がありました。各アプリが完全に独立していることです。1つを削除しても他には影響せず、インストール済みのアプリはWebCatalogデスクトップアプリがシステム上に残っていることに依存しません。
取り除きたかったのは、その裏側にある不要なデータの重複でした。
必要な仕組みはAPFSにすでに備わっていた
最近のMacは、AppleのAPFSファイルシステムを使用しています。APFSはクローンをサポートしています。これはコピーオンライトの仕組みで、独立した2つのファイルが、どちらかに変更が加えられるまで同じ物理データを共有できます。
300 MBのファイルをコピーする場合を想像してください。通常のコピーでは、macOSはさらに300 MBをディスクに書き込むため、2つのファイルは合計で約600 MBを消費します。
APFSクローンでは、新しいファイルも完全な300 MBのファイルに見え、そのように動作しますが、最初は元のファイルと同じ物理ブロックを参照します。
ファイルA ─────┐
├── 共有される物理ブロック
ファイルB ─────┘
後でファイルBの一部が変更されると、APFSは変更されたデータの分だけ新しいブロックを書き込みます。同一のままの部分は、引き続き元のブロックを共有できます。
ファイルA ───────── 共有ブロック
ファイルB ───────── 共有ブロック
└──────── 変更されたブロック
アプリケーションから見れば、ファイルAとファイルBは別々のファイルです。SSDから見れば、同一のデータは一度だけ保存すれば済みます。
これは、私たちが必要としていた動作とほぼ同じです。
共通のベースからアプリをビルドする
WebCatalogデスクトップアプリは現在、ElectronとPhotonのバージョンの組み合わせごとにベースを準備します。このPhotonベースアプリには、Electron、Photon、フレームワークの構造、共通の署名など、アプリ間で同一の部分が含まれます。
同じバージョンを使って別のアプリを作成する際、デスクトップアプリは、完全なコピーを一から展開してビルドすることはしません。代わりにPhotonベースアプリのAPFSクローンを作成し、異なる必要がある部分だけをカスタマイズします。
これには、アプリ名、アイコン、バンドル識別子、設定、実行ファイル名、ビルド識別子、その他のアプリ固有のメタデータが含まれます。
バンドルの大部分は変更されないため、APFSはElectronとPhotonを含む物理ブロックの共有を続けます。新たに容量を消費するのは、比較的小さなアプリ固有のデータだけです。
Electronだけを共有しないのはなぜか
もっと分かりやすい方法として、Electronを一度だけインストールし、すべてのアプリからそれを参照させることも考えられます。
私たちも検討しましたが、それでは信頼性の前提が根本的に変わってしまいます。共有のElectronがなくなれば、それに依存するすべてのアプリが動かなくなる可能性があります。キャッシュクリーナーによって削除されたり、WebCatalogデスクトップアプリのアンインストール時に削除されたりするかもしれません。また、個々のアプリの移動や復元も複雑になります。
さらに、古いバージョンを安全に削除できるよう、インストール済みのどのアプリがどのランタイムバージョンに依存しているかを追跡する必要もあります。
macOSのコード署名にも問題があります。厳格な署名検証では、アプリバンドルの外部を参照するシンボリックリンクは拒否されます。
APFSクローンなら、そのような依存関係を生じさせずに、共有の利点を得られます。インストール済みのアプリは、完全に独立したアプリケーションバンドルのまま、物理ディスクブロックを共有できます。
Photonベースアプリを削除しても、そこから作成したアプリは壊れません。インストール済みアプリを1つ削除しても、別のアプリには影響しません。アプリの構成は独立したまま、ファイルシステムがデータの共有を処理します。
コード署名によって節約効果がほぼ失われかけた
アプリのクローンは、課題の一部にすぎませんでした。macOSアプリケーションにはコード署名も必要です。
従来のビルド処理では、カスタマイズ後に各アプリケーションバンドルの内部まで再署名していました。通常のコピーなら問題ありません。しかし、コピーオンライトのストレージでは、大きなバイナリを変更すると、APFSがそのために新しい物理ブロックを割り当てることがあります。
開発中の計測では、Electronフレームワークの再署名によって、アプリごとに約194 MBの物理ストレージが増えました。アプリ作成時にはElectronのコピーを避けられたのに、署名時にその大部分を再び複製していたのです。
そこで、署名の手順も変更する必要がありました。
現在は、容量の大きい共通フレームワークをPhotonベースアプリの一部として署名します。デスクトップアプリがそのベースをクローンする際には、共有されている大きなバイナリを変更せず、アプリごとに異なる必要がある小さな部分だけを再署名します。
アプリが完成したら、完成したバンドルに対して厳格な署名検証を行い、すべてが有効であることを確認します。
これにより、macOSのコード署名による保証とAPFSによる容量節約を両立できます。
Node.jsにクローンを依頼しても、実際にはクローンされなかった
もう1つ予想外の問題がありました。クローン操作そのものです。
Node.jsには、COPYFILE_FICLONEやCOPYFILE_FICLONE_FORCEなど、コピーオンライト動作を要求するためのコピーフラグがあります。仕様上は、まさに必要としていたAPIに思えました。
Node.js 24を使ったテストでは、同じ288 MBのElectronアプリケーションを複数の方法でコピーし、それぞれの操作で物理ディスク使用量がどれだけ増えたかを計測しました。
fs.cpSync +288 MB
fs.cpSync + COPYFILE_FICLONE +288 MB
fs.cpSync + COPYFILE_FICLONE_FORCE +288 MB
/bin/cp -c -R ~0 MB
テストでは、Node.jsのどちらのクローンモードも物理的には完全なコピーを作成しました。FORCEを指定した場合でさえ、期待したクローンが行われなかったのにエラーは報告されませんでした。
macOS標準のcp -cは期待どおりに動作したため、WebCatalogデスクトップアプリではその仕組みを直接使用しています。
この経験は、ファイルシステムの最適化はファイルシステム自体を計測して確認すべきだという教訓にもなりました。コピーオンライトを要求するAPIを呼んだからといって、生成されたファイルが実際に物理ストレージを共有しているとは限りません。
最適化に失敗しても問題が起きないようにする
ディスク容量の節約は最適化です。一方、アプリのインストールを成功させることは必須です。
新しい処理は、クローンに失敗してもアプリのインストールに支障が出ないように設計しました。Photonベースアプリの準備に失敗した場合、キャッシュされたベースが破損している場合、クローンに失敗した場合、署名検証に通らなかった場合は、デスクトップアプリは完全な展開と完全な署名を行う従来のビルド方法に切り替えます。
したがって、最悪の場合でもディスク使用量が以前と同じになるだけで、インストールが失敗するわけではありません。
同時実行にも対応する必要がありました。Photonベースアプリは、まず一時ディレクトリで準備し、完成してから最終的な場所に移動します。これにより、2つのアプリのビルドが同時に行われても、ビルド途中のベースを誤って参照することはありません。
古いベースも安全に削除できます。インストール済みのAPFSクローンは、元のベースが残っていることに依存しません。ベースが削除されても、クローンが使用している物理ブロックは保持されます。
この仕組みは、最近のすべてのMacで標準となっているファイルシステム、APFSで機能します。アプリの保存先が外付けのHFS+やexFATドライブなど、クローンに対応していないディスクの場合、WebCatalogデスクトップアプリは通常のコピーに切り替えます。そのため、容量は節約できませんが、アプリはこれまでどおり動作します。WindowsとLinuxについては、現時点では変更ありません。
論理サイズと物理サイズは同じではない
少し紛らわしい副作用として、Finderでは各アプリが依然として約320 MBと表示される場合があります。
この数値はアプリの論理サイズを表します。アプリには実際に約320 MB分のファイルが含まれており、それらをクローンせずに別の場所へコピーすれば、おおむねその分のデータを書き込む必要があります。
変わったのは物理サイズ、つまりそれらのファイルが実際にディスク上で占める固有のブロック数です。
2つのアプリに同じ300 MBのフレームワークが含まれ、APFSによって基になるブロックを共有できる場合、各アプリの論理サイズにはそれぞれ300 MBが含まれますが、2つ目のアプリは新たな物理データをほとんど増やしません。
Finderの「情報を見る」やduなどのツールでは、この共有が必ずしも分かるとは限りません。節約効果は、ディスクに実際に残っている空き容量を見ると最も分かりやすくなります。
私たちは、Discord、Facebook、Instagram、Messenger、TikTok、そして2つのカスタムアプリという7つの実際のアプリで検証しました。この方法でビルドすると、従来のビルド方法と比べて約1.9 GBのディスク容量を節約できました。
また、基になる物理ディスク上のオフセットも調べ、共通のElectronとPhotonのデータがアプリ間でブロック単位で共有されていることを確認しました。従来の方法で作成したアプリはこれらのブロックを一切共有しておらず、比較対象として役立ちました。
結果
WebCatalogデスクトップアプリでアプリを1つだけ作る人にとっては、違いはわずかです。最初のアプリは、後続のクローン作成に使うPhotonベースアプリもデスクトップアプリが保持するため、以前よりもディスク使用量が少し増えます。
しかし、その後はアプリが増えるほど効果が急速に大きくなります。
変更前
1アプリ 約320 MB
10アプリ 3 GB超
APFSクローンを使うと、次のようになります。
変更後
1アプリ 約340 MB
10アプリ 約360 MB
最初のベースを作成した後は、アプリを1つ追加するごとに増える物理ディスク使用量は、通常わずか1~2 MBです。
重要なのは、共有ランタイムへの依存を導入せずにこれを実現できたことです。各アプリは通常の自己完結型macOSアプリケーションのままで、APFSが同一のElectronとPhotonのデータを一度だけ保存します。アプリを約10個インストールした場合、実際のディスク使用量を8分の1未満に抑えられます。
この機能は、macOS版WebCatalogデスクトップアプリの最新バージョンで利用できます。有効化の操作は不要です。新しく作成するアプリには自動的に適用され、既存のアプリも次回のアップデート時にディスク使用量が減ります。