We cut 24 MB off our download, and the failures taught us more
- Six releases took our installer from 312 MB to 288 MB, and the unpacked app from 560 MB to 501 MB.
- What came out: a deep-learning framework we never called, plus four scientific libraries pulled in as transitive dependencies.
- What stayed: two libraries that looked equally removable and were not.
- The most useful thing we learned is that the check we were using to decide could report success while being blind.
Our app bundles a Python runtime, so every library some dependency happens to import ships to every user forever. Most of them are never called. Removing them is free size, and the only question is which ones are genuinely unused.
The trap in how bundling works
PyInstaller lets you exclude a package by name. The catch is that an exclusion failure does not appear at build time. The build succeeds, the app looks fine, and the crash happens on a user's machine the first time something reaches for the missing import. A green build proves nothing about whether an exclusion was safe.
So the question becomes how to check. Our first instinct was to look in the bundle for the package directory. That check is worse than useless, because it is silently blind: compiled extensions land in a visible folder, but pure-Python packages are compressed into an archive where a directory listing will never show them. We were reading an empty result as proof of absence when the code was right there.
The fix is to search the archive's own table of contents, and to include a control: a package you know is present must come back non-zero in the same query. If your control returns nothing, your search is broken and every "not found" from it is meaningless. One of our own probes had exactly that fault, and its results were discarded rather than acted on.
What refused to leave
Two libraries looked like obvious candidates and stayed. One is a scientific stack that another dependency imports at runtime along a path that is only exercised on some hardware. The other is an internationalisation library reached indirectly through a chain that no static analysis of ours could rule out. In both cases we could remove them and get a clean build, and in neither case could we prove a user would not hit the missing import.
We kept them. A smaller download is worth having. It is not worth a crash that only shows up on someone else's Mac.
What this means for you
If you evaluate desktop AI apps by download size, know that the number is mostly a story about dependency hygiene rather than capability. Ours came down 8% without a single feature changing. Anyone can do the same, and anyone can do it carelessly and ship a crash instead.
Method
- Measured artefact: the signed DMG. 312 MB before, 288 MB after six releases. Unpacked app: 560 MB to 501 MB.
- Each removal shipped as its own release with the app's full test suite green, so a regression would be attributable to one change.
- Verification: search the bundled archive's table of contents for the excluded name, with a known-present package as a control in the same query.
- No model weights are included in these figures. Those download separately and are far larger.
Outlier is one signed download for Mac, with the models fetched separately when you pick one. The FAQ covers sizes per tier, and support is one person.