Sometimes a tweet thread isn't enough.
Yes, Proton VPN did reduce app size by 50 MB (actually closer to 40 MB). But the size they reduced should have never been in the app in the first place. It was introduced several months prior, when Proton made a change that helped improve its app launch time.
Let's walk through the changes that were made, why app startup improved, and how Proton can reduce app size even further.
Dynamic vs. Static Frameworks
This was Proton in v4.4.1 (Sept 2023). Most of the app is made up of dynamic frameworks like ProtonCore_UIFoundations.framework
. Understanding Dynamic vs. Static frameworks are key to Proton's changes, so we'll give a brief overview. You can read our blog on the subject to get more detail.
Dynamic frameworks let you share code between different app targets, which helps avoid duplication. But, there are two main performance tradeoffs to consider:
- Dynamic frameworks are linked at runtime by DYLD – the dynamic linker. As you use more dynamic frameworks, DYLD has to do more work, which can lead to increased pre-main time during app launch.
- Because dynamic frameworks are linked at runtime, this makes it hard for the compiler and linker to make certain optimizations. Most notably, there is limited dead code stripping because it's not possible to tell which symbols will be used externally, meaning the entirety of the library is included. If you're only using 1/100 functions in a dynamic framework, the other 99 functions will still ship in your app.
Static frameworks are linked at compile time and are typically faster and smaller than dynamic frameworks. The main performance tradeoff is that static frameworks cannot share code between app targets; you need to include them in every target you need the functionality in.
Proton's Size Changes
Back to Proton VPN. If we look at our Size tracker graph, we see the app more than double between v4.4.1 & v4.4.3
We can compare these two builds to see exactly what changed.
Our X-Ray diff is showing that Proton VPN removed every dynamic framework, which removed 95 MB of total size. But, we also see that static assets like ProtonCoreUIFoundationsResourcesiOS.bundle/Assets.car
(35 MB) were added in two app extensions + the main app target (+105 MB total). This asset catalog was already included in a dynamic framework in v4.4.1 so Proton VPN could have easily avoided the duplication by keeping the assets in a dynamic framework.
Here's our X-ray for v4.4.3 – everything in red is a duplicated file.
Proton went from having 20 MB of duplicated files to 135 MB of duplicated files in this one change. Most of the duplication is from static resources (images and localizations) and were previously being shared across targets as a dynamic framework.
But there's more to this change. Going back to the X-Ray diff, we can see the binary increased in two app extensions and the main app binary.
This is some of the dynamic frameworks now being linked as static frameworks in the respective targets. We can zoom in to see libraries like Lottie, Alamofire, and Sentry.
As a dynamic framework, Alamofire took up ~3 MB as opposed to 223 kB now. This is dead code stripping at work. Alamofire is roughly 220 kB in each of the three targets. In this case, the net size impact of including Alamofire three times is still smaller than it was as a dynamic framework.
Performance Impact
We ran a performance test between two versions of Proton VPN - one where the app was primarily dynamic frameworks and one where it was mostly static. As we'd expect, pre-main time (<early startup>
) decreased by 8%.
*This perf test was run between v5.7 & v 4.3. Proton VPN appeared to add a number of functions to its startup path. Because this test was not run between v4.4.3 & v4.4.1, we can't conclusively determine if dynamic → static improved total startup time, but that is the likely outcome.
Looking over this change, Proton was able to improve its pre-main launch time, but more than doubled in size, mainly from including a large asset catalog 3x. The net impact of duplicating libraries like Lottie, Sentry, and Alamofire as multiple static libraries is smaller than a single dynamic framework. A more optimal change for Proton VPN would have been to do something like keep the static assets like images & strings as a dynamic framework
The Recent Size Decrease
Which brings us to why we're here in the first place - Proton VPN reduced its app size by 50 MB between v5.7 & v5.8 (which we have as -37.4 MB reduction).
The reduction came from removing ~19 MB of vector files from the ProtonCore Asset catalog. In v5.8 the asset catalog is only duplicated twice now, once in the main app bundle and once in Quick Connect Widget
.
Here's Proton VPN now (v5.8)
30% of the app size is still from duplication introduced in the v4.4.3 change.
As for the initial tweet's sentiment of "let's give some praise," context is important here. Generally, Proton's decision to go from dynamic → static is probably the right one for their app. But in execution, Proton went "all or none". If Proton just kept the single dynamic framework with the large asset catalog, they would've gotten the best of both worlds (and still can).
Analysis Links
Here are the links to all build analysis pages referenced:
- v4.4.1 build analysis (September '23)
- v4.4.3 build analysis (October '23)
- v4.4.1 vs. v4.4.3 diff analysis
- v5.7 build analysis (October '24)
- v5.8 build analysis (October '24)
- v5.7 vs. v5.8 diff analysis