WebCatalog 如何利用 APFS 克隆将 Mac 应用的磁盘占用最多减少至原来的八分之一

WebCatalog 桌面应用现在在 macOS 上使用 APFS 克隆,大幅减少磁盘占用。通过仅存储一份相同的 Electron 和 Photon 数据,额外安装的应用通常只会增加 1–2 MB 的实际存储占用,而不是约 320 MB。

2026年9月23日

Nguyen Tran · Software Engineer

WebCatalog 如何利用 APFS 克隆将 Mac 应用的磁盘占用最多减少至原来的八分之一

过去,在 macOS 上使用 WebCatalog 桌面应用创建的每个应用大约会占用 320 MB 磁盘空间。对于基于 Electron 的应用来说,这并不罕见,但 WebCatalog 面向的是会把许多网页应用当作桌面应用使用的人。安装十个这样的应用,就可能占用超过 3 GB 空间,尽管这些应用中的大部分数据完全相同。

最近,我们改变了 WebCatalog 桌面应用在 macOS 上构建应用的方式。现在,第一个应用会占用约 340 MB 的实际磁盘空间,其中包括一份保存在本地、预先准备好的应用引擎副本。此后,每增加一个应用,通常只会增加 1–2 MB 的实际磁盘占用。在我们的测试中,十个应用的占用空间从超过 3 GB 降到了约 360 MB。

我们利用的是 macOS 已有的一项功能:APFS 克隆。关键并不只是克隆文件,而是让克隆机制在保持每个应用相互独立、代码签名正确,并且功能上与旧流程构建的应用一致的同时正常工作。

为什么每个应用都要额外占用 320 MB

WebCatalog 桌面应用可以将网站转换为独立的桌面应用。你创建的每个应用都运行在 Photon 上,这是我们基于 Electron 构建的应用引擎。在 macOS 上,每个应用都是一个普通的 .app 应用包,包含 Electron(Chromium 和 Node.js)、Photon,以及该应用专属的文件。

以 Slack 和 Discord 两个应用为例。它们的名称、图标、应用包标识符和配置各不相同,但体积庞大的 Electron 框架和 Photon 的大部分内容完全相同。

此前,每个应用都是作为一份完全独立的副本构建的。

Slack.app
  Electron
  Photon
  Slack 专属文件

Discord.app
  Electron
  Photon
  Discord 专属文件

即使两个应用中的 Electron 框架和 Photon 逐字节完全相同,macOS 仍会为每个应用存储另一份实际副本。因此,十个应用就意味着大约十份几乎相同、每份 320 MB 的应用包。

这种架构确实具有我们希望保留的一个特性:每个应用都完全独立。删除其中一个不会影响其他应用,已安装的应用也不依赖 WebCatalog 桌面应用继续存在于系统中。

我们想消除的是底层不必要的数据重复。

APFS 已经提供了我们需要的基础机制

现代 Mac 使用 Apple 的 APFS 文件系统。APFS 支持克隆:这是一种写时复制机制,允许两个独立文件共享相同的物理数据,直到其中一个文件发生变化。

假设复制一个 300 MB 的文件。普通复制会让 macOS 再向磁盘写入 300 MB,因此两个文件总共占用约 600 MB。

使用 APFS 克隆时,新文件看起来、用起来仍然是一个完整的 300 MB 文件,但起初它指向与原文件相同的物理数据块。

文件 A ─────┐
            ├── 共享的物理数据块
文件 B ─────┘

如果文件 B 的一部分后来发生变化,APFS 只会为变化的数据写入新的数据块。所有保持相同的部分都可以继续共享原有数据块。

文件 A ───────── 共享数据块

文件 B ───────── 共享数据块
       └──────── 已变更的数据块

从应用的角度看,文件 A 和文件 B 是两个独立文件;从 SSD 的角度看,相同的数据只需存储一次。

这几乎正是我们需要的行为。

从共同的基础应用构建应用

现在,WebCatalog 桌面应用会为每一种 Electron 与 Photon 版本组合准备一个基础应用。这个 Photon 基础应用包含各应用之间相同的部分,包括 Electron、Photon、框架结构和通用签名。

桌面应用使用相同版本创建另一个应用时,不再从头解压并构建另一份完整副本,而是克隆 Photon 基础应用,再仅对需要区别于其他应用的部分进行定制。

这些部分包括应用名称、图标、应用包标识符、设置、可执行文件名称、构建标识符以及其他应用专属元数据。

由于应用包的大部分内容保持不变,APFS 会继续共享存有 Electron 和 Photon 的物理数据块。只有相对少量的应用专属数据会占用新空间。

为什么不直接共享 Electron?

一种更显而易见的做法是只安装一次 Electron,再让每个应用都指向它。

我们考虑过这种方案,但它会从根本上改变可靠性模型。如果共享的 Electron 安装文件消失,所有依赖它的应用都可能停止工作。缓存清理工具可能会删除它,卸载 WebCatalog 桌面应用也可能会删除它,而且移动或恢复单个应用会变得更复杂。

这种方案还要求我们追踪哪些已安装应用依赖哪些运行时版本,以便安全地清理旧版本。

macOS 代码签名还带来了另一个问题:严格签名验证会拒绝指向应用包外部的符号链接。

APFS 克隆让我们获得共享带来的好处,却不必引入这种依赖关系。已安装的应用可以共享物理磁盘数据块,同时仍是完整、独立的应用包。

删除 Photon 基础应用不会破坏由它创建的应用。删除某个已安装应用也不会影响其他应用。共享由文件系统负责处理,而应用架构仍保持独立。

代码签名差点抵消节省的空间

克隆应用只是问题的一部分。macOS 应用还需要进行代码签名。

旧的构建流程会在定制完成后,对每个应用包进行深度重新签名。对于普通副本,这没有问题。但在写时复制存储机制下,修改大型二进制文件可能导致 APFS 为其分配新的物理数据块。

在开发过程中,我们测得,仅重新签名 Electron 框架就会让每个应用额外占用约 194 MB 的物理存储空间。我们虽然在创建应用时避免了复制 Electron,却又在签名时复制了其中的大部分内容。

因此,签名流程也必须改变。

现在,大型的通用框架会在 Photon 基础应用中完成签名。桌面应用克隆这个基础应用时,我们避免修改这些大型共享二进制文件,只对应用之间确实需要有所区别的较小部分重新签名。

应用构建完成后,我们会对最终的应用包运行严格签名验证,确保所有签名仍然有效。

这样,我们既能保留 macOS 的代码签名保障,也能保留 APFS 带来的空间节省。

让 Node.js 执行克隆,却没有真正克隆

还有一个意料之外的问题:如何执行克隆操作本身。

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 基础应用会先在临时目录中准备好,待构建完成后才移到最终位置。这样,即使同时构建两个应用,也不会意外地读取到尚未构建完成的基础应用。

旧的基础应用也可以安全删除。已安装的 APFS 克隆不依赖原始基础应用继续存在。如果基础应用被删除,克隆仍会保留它所使用的物理数据块。

这一机制适用于 APFS,即所有现代 Mac 的默认文件系统。如果你的应用位于无法进行克隆的磁盘上,例如使用 HFS+ 或 exFAT 格式的外置硬盘,WebCatalog 桌面应用会回退到普通复制。应用仍会像以前一样工作,但不会节省空间。Windows 和 Linux 目前没有变化。

逻辑大小与物理大小不是一回事

一个可能让人有些困惑的副作用是,访达仍可能显示每个应用约为 320 MB。

这个数字表示应用的逻辑大小。应用确实包含大约 320 MB 的文件;如果不通过克隆将这些文件复制到别处,大致就需要写入这么多数据。

改变的是物理大小,也就是这些文件实际上在磁盘上占用了多少独有的数据块。

如果两个应用包含相同的 300 MB 框架,而 APFS 允许它们共享相同的底层数据块,那么每个应用在逻辑上都可以包含 300 MB,但第二个应用几乎不会增加新的物理数据占用。

访达的“显示简介”窗口和 du 等工具不一定会显示这种共享。节省的空间在磁盘实际剩余可用空间上最为明显。

我们用七个真实应用验证了这一点:Discord、Facebook、Instagram、Messenger、TikTok,以及两个自定义应用。与旧的构建流程相比,用这种方式构建它们节省了约 1.9 GB 磁盘空间。

我们还检查了底层物理磁盘偏移量,确认这些应用在数据块层面共享了通用的 Electron 和 Photon 数据。使用旧构建流程创建的应用没有共享任何这些数据块,为我们提供了有用的对照。

最终效果

对于只用 WebCatalog 桌面应用创建一个应用的人来说,差别不大。第一个应用实际上比以前略占空间,因为桌面应用还会保留用于创建后续克隆的 Photon 基础应用。

但之后,收益会迅速增加。

以前

1 个应用       ~320 MB
10 个应用      >3 GB

使用 APFS 克隆后:

现在

1 个应用       ~340 MB
10 个应用      ~360 MB

创建好初始基础应用后,每增加一个应用,通常只会增加 1–2 MB 的实际磁盘占用。

重要的是,我们在不引入共享运行时依赖的情况下实现了这一点。每个应用仍是普通、独立完整的 macOS 应用,而 APFS 只需存储一份相同的 Electron 和 Photon 数据。安装约十个应用时,实际磁盘占用可降至原来的八分之一以下。

这一功能已在最新版 macOS 版 WebCatalog 桌面应用中提供。你无需手动开启:新应用会自动使用它,已有应用也会在下次更新时缩减占用空间。