The size of mobile applications directly impacts many factors, from user experience and download rates to even the app's ranking in the market. For years, while developing mobile apps for both my side projects and large client projects, the topic of "app size" has always been a key focus for me. People, especially when downloading over mobile data, are reluctant to download unnecessarily large applications.
For me, app size optimization isn't just a matter of performance; it's also a matter of cost. A smaller app package means faster downloads, less data usage, and consequently, a better first impression. In this post, I'll share the practical approaches I've implemented in mobile app development that genuinely work, the challenges I've faced, and the trade-offs I've chosen.
The Impact and Optimization of Static Resources
One of the factors that most significantly affects app size is, undoubtedly, static resources. Images, audio files, videos, and other media content can constitute a substantial portion of an app's total size. In a mobile application I developed for an internal platform at a bank, the app's initial size was around 80 MB due to high-resolution images on the onboarding screens.
I found this unacceptable and immediately began optimizing the images. The first step was to identify unnecessarily high-resolution images. Often, designers aim to provide us with "the best," but they might provide files that are unnecessarily large for mobile device screen resolutions.
đź’ˇ Image Format Selection
Modern image formats offer much better compression ratios compared to older JPEG/PNG. Using WebP (common on Android) and AVIF formats, in particular, can significantly reduce file sizes. SVG is ideal for vector-based images, preserving quality regardless of resolution.
In one project, I converted all PNG images to WebP, reducing the size attributable to images alone by 40%. On the iOS side, using Asset Catalogs allows for providing different resolutions specific to the device and eliminating unnecessary ones. Furthermore, using tools like ImageOptim to clean image metadata and apply lossless compression can save a few more MB. For instance, I've managed to reduce a 10 MB PNG file to as little as 3 MB using TinyPNG or Squoosh.
# Example command for WebP conversion (with cwebp on Linux)
for file in *.png; do
cwebp -q 75 "$file" -o "${file%.png}.webp"
done
With this approach, I managed to reduce the size of the aforementioned bank application from 80 MB to 45 MB. This not only improved the download size but also positively impacted the app's loading time.
Code Minification and Tree-Shaking
The application code itself can also cause bloat, especially when large libraries and frameworks are used. Unused code snippets, and even entire libraries we import, can be included in the final package. This is where the concept of "tree-shaking" comes into play.
On the Android side, tools like ProGuard and R8 automatically remove unused classes, methods, and fields during compilation, thereby shrinking the code. In the mobile operator screens I developed for an ERP system at a manufacturing company, the initial version of the app was 35 MB. By making the R8 optimizations more aggressive, this size was reduced to 22 MB.
⚠️ ProGuard/R8 Considerations
Code shrinking tools can sometimes accidentally remove dynamically used code (like methods called via reflection). Therefore, carefully configuring the
proguard-rules.profile and conducting comprehensive tests is critically important. Otherwise, you might encounter strangeClassNotFoundExceptionorNoSuchMethodErrorerrors in production.
Frameworks like Flutter also have similar mechanisms. It's possible to reduce the APK size by putting debug symbols into a separate file using the flutter build apk --split-debug-info=./debug_info command. This allows you to manually load symbols during debugging but lightens the end-user package. In my Android spam application, I achieved about 3-4 MB of savings with this method.
# Additional configuration in pubspec.yaml file
flutter:
# ...
# The setting below helps reduce your app's size
# Removes unnecessary icon fonts
uses-material-design: true
generate: true
Furthermore, removing unnecessary libraries and dependencies from the project is also important. Every added library brings its own dependencies and code. Instead of including an entire library for a small part of a feature, I prefer to implement that feature manually or find a lighter alternative. Correctly using different dependency types like api or compileOnly instead of implementation in the build.gradle file also makes a difference in this regard.
Native Libraries and ABI Splitting
Mobile applications often contain native libraries to support different CPU architectures: such as armeabi-v7a, arm64-v8a, x86, x86_64. If your application includes libraries for all these architectures, the package size unnecessarily grows because a device only uses the library appropriate for its own architecture.
To solve this problem, there are two main approaches on the Android side: APK Splitting and Android App Bundle (AAB). I now prefer AAB. When you upload your app to Google Play using AAB, Google Play delivers an optimized APK specifically for each device. An APK containing only the necessary resources is downloaded based on the device's architecture, language, and screen density.
ℹ️ Using Android App Bundles (AAB)
AAB is a more modern and flexible solution compared to APK Splitting. Developers don't need to create and upload separate APKs for each architecture; a single AAB file handles everything. In my experience, this has led to an average download size reduction of 15-20%.
In a client project, when I was doing manual APK Splitting, I had configured the build.gradle file like this:
android {
// ...
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a' // Only include these architectures
universalApk false // Do not create a universal APK including all architectures
}
}
}
This code snippet creates separate APKs for the specified ABIs. However, if you are using AAB, this manual setting is generally not needed, as AAB handles this optimization automatically. In my production ERP system, after switching to AAB, I observed that the average size of the application downloaded by users decreased from 18 MB to 14 MB. This represented a significant improvement, especially for older Android devices, in terms of download and installation times. Additionally, completely removing unnecessary native libraries from the project is also important. Sometimes a dependency brings along a native library that we never use.
Language Resources and Localization Optimization
When developing multilingual applications, language resources can also increase the app's size. If your app supports dozens of different languages and you include all strings, images, and other resources for each language in the package, this can lead to significant bloat.
In my practice, I generally prefer to include only the most commonly used languages directly in the package. For other languages, I either use dynamic download mechanisms or don't include them at all, depending entirely on business requirements. For example, in a mobile application I released in Europe, 15 language supports were required. Including all languages in the package increased the app's size by approximately 5 MB.
🔥 Unnecessary Language Resources
Not only text resources for each language but sometimes language-specific images or audio files can also exist. Therefore, it's necessary to review folders like
res/drawable-xxas well as checking folders likeres/values-xx/strings.xml.
On Android, we can ensure that only specific language resources are included in the package using the resConfigs or resourceConfigurations setting. This prevents less commonly used languages from being unnecessarily included.
android {
// ...
defaultConfig {
// ...
resConfigs "en", "tr", "de" // Only include English, Turkish, and German resources
}
}
When I used this setting in the financial calculators of my side product, I saw an approximate 2 MB reduction in the app's total size. While this may seem like a small amount, when multiple optimization strategies are combined, they can make a significant overall difference. Additionally, cleaning up duplicated or empty strings in translation files is also a beneficial step. Sometimes translators might re-write the same text in different places or leave some texts empty. These situations also cause unnecessary data bloat.
Build Processes and Compiler Optimizations
The application's build process directly impacts the size of the final package. There are significant differences between development (debug) and release builds, and managing these differences correctly is essential. Debug builds typically include debugging information, unnecessary logs, and sometimes less optimized code.
In release builds, most of this information should be stripped, and the highest possible optimizations should be applied by the compiler. For example, on Android, setting minifyEnabled and shrinkResources to true significantly reduces size by removing unused code and resources.
android {
// ...
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
When distributing a mobile application for a production ERP system via a CI/CD pipeline, I ensured that release builds were configured correctly. Initially, I had forgotten the shrinkResources setting, and the APK size was unnecessarily large. After activating this setting, the app size was further reduced by approximately 10-15%.
đź’ˇ Build Cache and Incremental Compilation
To shorten build times and reduce resource usage, I leverage features like build cache and incremental compilation. Especially in large projects, these settings can speed up the development cycle while indirectly having a positive impact on the final package size. Faster builds mean more frequent optimization attempts.
Similar compiler settings are available on the iOS side. Options like "Strip Debug Symbols During Copy," "Strip Style," and "Deployment Postprocessing" in Xcode affect the final size of the application. I always configure these settings as aggressively as possible for release builds. In my opinion, such fine-tuning details are critical as they directly influence the app's performance and its speed of reaching users.
Run-Time Loading and Dynamic Feature Modules
One of the most advanced approaches to optimizing app size is to load only the features when they are needed, rather than downloading all features of the application at startup. This can be a game-changer, especially for large and feature-rich applications. Google Play Feature Delivery (dynamic feature modules) on Android and On-Demand Resources (ODR) on iOS are used for this purpose.
In one of my side products, I chose to download some advanced analytics and reporting features only when the user clicked on the relevant menu, rather than loading them at startup. These features were not critical for the app's core flow and were rarely used by most users. This allowed me to reduce the app's initial download size by approximately 7 MB.
ℹ️ Advantages of Dynamic Feature Modules
Dynamic feature modules not only reduce application size but also modularize the development process. Each feature can be developed and tested in its own module. This is particularly beneficial for large teams and helps reduce technical debt in the long run.
On the Android side, this is done by creating separate feature modules in addition to the base module. These modules are delivered to the device by the Play Store only when requested. For example, in a production ERP system, I considered offering an AI-powered production planning module as a dynamic feature. This is because this feature would not be used by all operators, but only by specific planning managers.
<!-- build.gradle (feature module) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
package="com.example.myfeature">
<dist:module
dist:instant="false"
dist:onDemand="true"
dist:title="@string/feature_title">
<dist:fusing dist:include="true" />
</dist:module>
</manifest>
This approach is very sensible, especially for features requiring large data sets or specific libraries. For example, in my Android spam application, I allowed some advanced filtering models to be downloaded only by pro users, thus avoiding bloating the download package for free users. Of course, this introduces additional complexity: managing the loading status of modules, handling download errors, and providing appropriate feedback to the user. However, the benefits gained are worth this complexity.
Conclusion
Mobile app size optimization is not a problem that can be solved with a single magic wand. It's a process that requires continuous attention, a combination of multiple strategies, and sometimes painful trade-offs. Every choice we make in many areas, from image optimization to code minification, native library management to dynamic feature modules, affects the app's final size.
In my experience, although these optimizations might seem like gains of just a few MBs, when combined, they directly impact the app's overall performance, user experience, and even market success. Remember, every MB matters. Especially in developing markets or regions with low bandwidth, a small app size significantly increases the chances of your app being downloaded and used. Therefore, with every new release or major feature addition, I revisit these optimization steps.
Top comments (0)