Apps on the ultimate diet: beyond stripping Universals

In yesterday’s exercise in trying to reduce the size of a Universal App, I showed how you can strip the Mach-O executable in a Universal App so that it supports only one architecture. As I pointed out, I deliberately ignored those dylibs stored in the app’s Contents/Frameworks folder, which is what I consider today.

Application bundles have the following structure within the Contents folder:

  • _CodeSignature, a folder containing the file CodeResources for signature checks;
  • CodeResources, a file recording the notarization ticket;
  • Frameworks, an optional folder containing framework folders and dylibs;
  • Info.plist, the app’s main Property List;
  • MacOS, a folder containing the app’s main executable code in a Mach-O binary;
  • PkgInfo, eight bytes containing the classic type and identifier bytes;
  • Resources, a folder containing predominantly non-executable resources for the UI, such as interface nibs, icons, and localisations.

The thinning performed using lipo, detailed in the previous article, only tackles the main executable code. What about the frameworks, which can also be Universal?

Many apps contain significant frameworks, and they’re essential in those which have been written in Swift, to provide the ‘glue’ between system calls made using the Swift language and the system itself. You’ll typically see 18 dylibs with names prefaced by libswift in each app which has been built from Swift source code, adding 11.4 MB to the size of every Swift app.

When you build a Universal App which requires a minimum of macOS 11, if it’s written in Swift and uses no custom frameworks, there’s no Frameworks folder generated, and the total size of that is around 11.4 MB smaller than the same app built to support earlier versions of macOS. This is apparently because macOS 11 now incorporates that ‘glue’ in system files, so sharing those frameworks.

If you use third-party command tools written in Swift, you may recall something similar happening back in macOS 10.14.4. Prior to that, command tools built in Swift needed supporting libraries, which made them large. In 10.14.4, those were incorporated into the system by including them in /usr/lib/swift, saving space in each command tool installed.

Look at those Swift frameworks using a utility like ArchiChect, and you’ll see that they’re Intel-only, and aren’t used by Apple Silicon Macs unless the app is run in its Intel version under Rosetta 2. So, if we were to create an ARM64-only version of a Universal App, couldn’t we dispense with those redundant frameworks? Unfortunately not: removing them causes a serious signature error, and macOS won’t allow our superslim ARM64-only app to even launch. This means that the maximum saving available in using lipo to thin a Universal App is half the size of its Mach-O Universal Binaries, nothing more.

I’m now ready to give guidelines on expected app sizes in different situations.

If you have a Universal App which you want to slim by making it work on only one architecture using lipo, you should expect to see a saving of half the size of its Mach-O Universal Binaries, which normally means the executable file in Contents/MacOS. Other contents of the app bundle, including its frameworks, are unlikely to be amenable to lipo‘s thinning, and consequently will remain the same size.

So in the case of Mints, its total size as a Universal App is 17.2 MB, with an executable of 442 KB. A stripped Intel-only version has its executable reduced to 214 KB, and a stripped ARM-only version has an executable of 229 KB. Savings achieved by stripping are little more than 200 KB, or just over 1%.

If you’re a developer, and your Universal App will support versions of macOS prior to Big Sur, then that Universal App is likely to be the same size as its current Intel version, with the addition of the total size of all Mach-O files which will be converted to Universal Binaries (as they effectively double in size). The latter excludes any Swift frameworks, which won’t be added to support the ARM64 architecture. In the case of Mints, that adds around 200 KB, or just over 1%.

If you’re only going to support Big Sur and later versions of macOS, your app won’t require the 11.4 MB of Swift frameworks. In the case of Mints, that far outweighs the 200 KB cost of making it Universal, as dropping the Swift frameworks saves 65% of the app’s original size. For Swift apps whose Intel executables total less than 11 MB, a macOS 11-only Universal App should always be smaller than the original.

Big Sur giveth, and Big Sur taketh away.