Performance Optimization

Reducing App Size: Tree Shaking, Deferred Loading, and Asset Optimization

16 min Lesson 12 of 12

Reducing App Size in Flutter

A smaller release bundle loads faster, consumes less storage, and reduces drop-off on low-bandwidth networks. Flutter gives you several first-class tools to shrink your APK, AAB, or IPA: tree shaking (dead-code elimination), deferred component loading (splitting the app into on-demand chunks), asset optimization (compressing images and fonts), and the built-in size analysis report from flutter build --analyze-size.

Note: Always measure size on a release build. Debug builds include the Dart VM, observatory, and unminified symbols — they are typically 4–10x larger than a release build and are not representative of what users download.

1. Tree Shaking — Dead-Code Elimination

Dart's compiler performs tree shaking automatically when you compile in release mode (flutter build apk --release). Tree shaking analyzes your import graph and removes every class, function, and constant that is never reachable from main(). This is why importing a heavy package but only using one utility from it does not necessarily bloat your bundle — unused symbols are stripped.

  • Tree shaking works best when packages export individual files rather than a single barrel file.
  • Reflection (mirrors) and dart:mirrors disable tree shaking for the affected code — avoid them in production.
  • Code generated by build_runner is fully tree-shakeable as long as references are static.

Verifying Tree Shaking Eliminates Unused Code

// math_utils.dart — a library with two top-level functions
double square(double x) => x * x;
double cube(double x) => x * x * x;  // never called anywhere

// main.dart
import 'math_utils.dart';

void main() {
  print(square(4));  // only square() is reachable
  // cube() is unreachable, so Dart tree-shaking removes it from the bundle
}

2. Deferred Loading (Lazy Imports)

For features that are rarely used — onboarding flows, help screens, admin panels — you can split them into a deferred library. The Dart compiler emits a separate .js or native snapshot for each deferred library, and the runtime downloads or loads it only when the application actually needs it. On Flutter Web this maps directly to JavaScript code splitting; on mobile it is supported via deferred components on Android (Android App Bundles + Play Feature Delivery).

Deferred Import Syntax

import 'package:myapp/features/onboarding/onboarding_screen.dart'
    deferred as onboarding;

class HomeScreen extends StatelessWidget {
  Future<void> _launchOnboarding(BuildContext context) async {
    // Library is loaded from disk / network here, only on first call
    await onboarding.loadLibrary();
    if (!context.mounted) return;
    Navigator.of(context).push(
      MaterialPageRoute<void>(
        builder: (_) => onboarding.OnboardingScreen(),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ElevatedButton(
          onPressed: () => _launchOnboarding(context),
          child: const Text('Start Onboarding'),
        ),
      ),
    );
  }
}
Tip: Call loadLibrary() as early as possible — for example, in initState() — so the library is already cached by the time the user taps the button. loadLibrary() is idempotent: calling it multiple times is safe.

3. Asset Optimization

Images are frequently the biggest contributor to bundle size. Apply these strategies before shipping:

  • Use WebP or AVIF instead of PNG/JPEG. WebP offers 25–35 % smaller files at equivalent visual quality. Convert with cwebp or Squoosh.
  • Resize to display resolution. Bundling a 4 K photo that is rendered at 200 × 200 logical pixels wastes roughly 400x the necessary data. Use the closest 1x, 2x, 3x variants.
  • Subset fonts. If you ship a custom font, use a tool like pyftsubset to keep only the Unicode ranges your app actually uses. A full Latin + Arabic font can be trimmed from 500 KB to under 50 KB.
  • Remove unused assets. Assets declared in pubspec.yaml under flutter: assets: are always bundled, whether or not they are loaded at runtime.

4. Reading the Size Analysis Report

The --analyze-size flag produces a detailed breakdown of every compiled package and asset in your release bundle. Run it like this:

Generating the Size Analysis Report

# Android App Bundle (recommended for Play Store)
flutter build appbundle --release --analyze-size

# iOS (requires Xcode toolchain)
flutter build ios --release --analyze-size

# Output location printed at the end of the build, e.g.:
# A summary of your APK analysis can be found at:
# /Users/you/.flutter-devtools/apk-code-size-analysis_01.json

# Open in Dart DevTools for an interactive treemap:
dart devtools
# Then open the saved .json file in the "App Size" tab

The report groups symbols by package → library → class → member. The largest nodes are your optimization targets. Common findings include:

  • Packages that bring in large transitive dependencies (e.g., Firebase, Google Maps).
  • Generated code (JSON serialization, routes) that is larger than expected.
  • Unused icon fonts bundled in full (e.g., MaterialIcons — Flutter already tree-shakes icon data by default since Flutter 2.3).
Warning: The --analyze-size flag compiles with additional instrumentation that slightly increases build time. Do not use it in a normal CI pipeline — reserve it for periodic size audits.

5. Additional Size-Reduction Techniques

  • Build split APKs by ABI: flutter build apk --split-per-abi generates separate APKs for arm64-v8a, armeabi-v7a, and x86_64, each containing only the native library for that architecture.
  • Obfuscate Dart code: --obfuscate --split-debug-info=./debug-info strips readable symbol names, reducing snapshot size by 5–15 % and hardening against reverse engineering.
  • Avoid bundling test assets: Keep test fixtures in test/, not in assets/ — only assets/ entries in pubspec.yaml are included in the bundle.

Summary

Reducing Flutter app size is a multi-layered discipline. Tree shaking is automatic but relies on static, non-reflective code. Deferred loading lets you split rarely-used features into on-demand chunks. Asset optimization — correct image formats, resolution variants, and font subsetting — often yields the largest absolute savings. Finally, flutter build --analyze-size with Dart DevTools gives you a visual treemap to find and eliminate the biggest contributors to bundle bloat.

Key Takeaway: Profile before optimizing. Use --analyze-size to identify the real bottlenecks, apply targeted fixes (deferred imports for large features, WebP for images, ABI splits for native libraries), and re-measure to confirm the improvement.