Mobile Development 38 min read

Flutter iOS Package Size Optimization: Analysis and Implementation

The article examines why adding Flutter to an iOS app inflates the binary, breaks down the framework’s largest parts—engine, AOT snapshot, assets, and ICU data—and details a three‑pronged strategy of compression, externalization and stripping, using custom build scripts to move or delete files and dynamically load data, achieving roughly a 12 MB size reduction.

Tencent Music Tech Team
Tencent Music Tech Team
Tencent Music Tech Team
Flutter iOS Package Size Optimization: Analysis and Implementation

1. Background

Flutter is an excellent cross‑platform solution that our QMusic team follows closely. While integrating Flutter into QMusic Live, the primary issue we faced was the increase in package size. This article analyzes the Flutter package size problem step by step, explores every possible optimization point, combines real‑project experience and engine source code, and finally provides a detailed implementation plan for size reduction. Readers are welcome to discuss Flutter‑related technologies.

1.1 Flutter Hybrid Development Mode

To add Flutter to an existing native app, there are two common integration methods:

Include the native project as a sub‑project of the Flutter project, managed entirely by Flutter (unified management mode).

Keep the native project unchanged and add the Flutter project as a separate module (three‑side separation mode), where Flutter is developed independently.

The unified management mode is easy to set up but has obvious drawbacks: heavy coupling among Android, iOS and Flutter code, and a significant increase in tool‑chain time, which reduces development efficiency. Therefore we adopt the three‑side separation mode, using AAR for Android and Pod for iOS to integrate Flutter in a lightweight manner.

1.2 Flutter Size‑Reduction Requirement

Introducing Flutter inevitably enlarges the app package. In the three‑side separation mode, the size increase on Android is the Flutter AAR, and on iOS it is the Flutter framework. To solve the size problem we need to optimize both the AAR and the framework.

1.3 Development Environment

Flutter 1.17.1 • channel stable

macOS 10.15.4

Xcode 11.4 (iOS & macOS development)

Python 2.7.16

2. iOS Framework Product Analysis

In our project the Flutter code is compiled into a framework, which is then linked into the iOS host app. To reduce the size we first analyze the framework contents.

2.1 Framework Structure

Using the tree command on the Release directory we see two frameworks:

Release
    ├── App.framework
    │   ├── App            // AOT snapshot data compiled from Dart business code (Mach‑O dynamic library)
    │   ├── Info.plist
    │   └── flutter_assets // resource files
    └── Flutter.framework
        ├── Flutter        // Flutter engine (Mach‑O dynamic library)
        ├── Headers
        ├── Info.plist
        ├── Modules
        ├── _CodeSignature
        └── icudtl.dat     // internationalization data

The large files are highlighted in the diagram.

2.2 Framework Size Analysis (Release)

The following table lists the biggest files in the Release directory, which are the main targets for size optimization.

Name

Size

Description

App

7.3 M

Dart business code AOT compilation product

flutter_assets

2 M

Images, fonts and other resources

Flutter

11 M

Engine

icudtl.dat

884 k

Internationalization support file

Other third‑party plugins

800 k

e.g., Flutter_boost

3. iOS Size‑Reduction Strategy

From the previous analysis the four biggest components are App , flutter_assets , Flutter and icudtl.dat . We will examine each for possible reduction.

Optimization directions are divided into three categories:

Compress : data compression reduces framework size but has limited impact on the final app because the app package is already compressed.

Delete : remove unused parts.

Move : if removal is not possible, separate the component and load it dynamically at runtime.

3.1 App.framework / flutter_assets

The flutter_assets directory contains resources. If we do not want to bundle it into the app, we can move it out and download it on demand. Two methods are available:

Modify the Flutter‑tools build script so that flutter_assets is excluded during framework generation.

Post‑process the generated framework and strip flutter_assets via a custom script.

We currently use the second method because modifying the build script for a single asset is not cost‑effective.

Removing flutter_assets does not affect engine startup as long as the correct path is supplied to the engine. The engine checks for the assets directory in FlutterDartProject.mm (function DefaultSettingsForProcess ) and stores the path in settings.assets_path . Therefore the assets can reside inside the framework or be downloaded externally, provided the engine is informed of the correct location.

//FlutterDartProject.mm
// Checks to see if the flutter assets directory is already present.
if (settings.assets_path.size() == 0) {
  NSString* assetsName = [FlutterDartProject flutterAssetsName:bundle];
  NSString* assetsPath = [bundle pathForResource:assetsName ofType:@""];
  if (assetsPath.length == 0) {
    assetsPath = [mainBundle pathForResource:assetsName ofType:@""];
  }
  if (assetsPath.length == 0) {
    NSLog(@"Failed to find assets path for \"%@\"", assetsName);
  } else {
    settings.assets_path = assetsPath.UTF8String;
    // further checks omitted for brevity
  }
}

3.2 Flutter.framework / icudtl.dat

icudtl.dat stores the engine’s internationalization data. It is loaded during engine initialization via the icu_data_path field in Settings . By providing an external path we can move this file out of the framework.

//settings.h
bool icu_initialization_required = true;
std::string icu_data_path; // path to icudtl.dat
//shell.cc (simplified)
if (settings.icu_initialization_required) {
  if (settings.icu_data_path.size() != 0) {
    fml::icu::InitializeICU(settings.icu_data_path);
  } else if (settings.icu_mapper) {
    fml::icu::InitializeICUFromMapping(settings.icu_mapper());
  } else {
    FML_DLOG(WARNING) << "Skipping ICU initialization in the shell.";
  }
}

3.3 App.framework / App (AOT snapshot)

The App binary contains the AOT‑compiled Dart code. Its large size comes from four symbols:

kDartIsolateSnapshotData

kDartVmSnapshotData

kDartIsolateSnapshotInstructions

kDartVmSnapshotInstructions

iOS does not allow the executable part ( *_Instructions ) to be loaded dynamically, but the data sections ( *_Data ) can be loaded at runtime. Therefore we can move kDartIsolateSnapshotData and kDartVmSnapshotData out of the binary and load them dynamically.

// nm output (simplified)
000000000065c008 b _kDartIsolateSnapshotBss
00000000003a1270 S _kDartIsolateSnapshotData
0000000000009000 T _kDartIsolateSnapshotInstructions
000000000065c000 b _kDartVmSnapshotBss
0000000000399400 S _kDartVmSnapshotData
0000000000004000 T _kDartVmSnapshotInstructions

Only the data sections can be externalized. The generation of these sections is performed by the gen_snapshot tool. By customizing gen_snapshot we can write the data to external files instead of embedding them.

// image_snapshot.cc (excerpt)
void WriteTextToLocalFile(WriteStream* clustered_stream, bool vm) {
#if defined(TARGET_OS_MACOS_IOS)
  auto OpenFile = [](const char* filename) {
    bin::File* file = bin::File::Open(NULL, filename, bin::File::kWriteTruncate);
    if (file == NULL) {
      exit(255);
    }
    return file;
  };
  // Choose path based on architecture and vm/isolate flag
#if defined(TARGET_ARCH_ARM64)
  bin::File* file = OpenFile(vm ? "./SnapshotData/arm64/VmSnapshotData.S"
                               : "./SnapshotData/arm64/IsolateSnapshotData.S");
#else
  bin::File* file = OpenFile(vm ? "./SnapshotData/armv7/VmSnapshotData.S"
                               : "./SnapshotData/armv7/IsolateSnapshotData.S");
#endif
  // Write the assembly representation to the file (omitted for brevity)
#endif
}

void AssemblyImageWriter::WriteText(WriteStream* clustered_stream, bool vm) {
#if defined(TARGET_OS_MACOS_IOS)
  WriteTextToLocalFile(clustered_stream, vm);
#else
  // original in‑framework embedding logic
#endif
}

After extracting the .S files we compile them into raw data files:

# Example for armv7
xcrun cc -arch armv7 -c ./SnapshotData/armv7/IsolateSnapshotData.S -o ./SnapshotData/armv7/HeadIsolateData.dat
xcrun cc -arch armv7 -c ./SnapshotData/armv7/VmSnapshotData.S -o ./SnapshotData/armv7/HeadVMData.dat
# Strip the object file header (first 312 bytes)
tail -c +313 ./SnapshotData/armv7/HeadIsolateData.dat > ./SnapshotData/armv7/IsolateData.dat
tail -c +313 ./SnapshotData/armv7/HeadVMData.dat > ./SnapshotData/armv7/VMData.dat

These IsolateData.dat and VMData.dat files are then loaded by the engine via modified ResolveVMData and ResolveIsolateData functions.

// dart_snapshot.cc (excerpt)
static std::shared_ptr
ResolveVMData(const Settings& settings) {
#if OS_IOS
  if (settings.ios_vm_snapshot_data_path.empty()) {
    return SearchMapping(settings.vm_snapshot_data,
                         settings.vm_snapshot_data_path,
                         settings.application_library_path,
                         DartSnapshot::kVMDataSymbol,
                         false);
  } else {
    return SetupMapping(settings.ios_vm_snapshot_data_path);
  }
#else
  return SearchMapping(settings.vm_snapshot_data,
                       settings.vm_snapshot_data_path,
                       settings.application_library_path,
                       DartSnapshot::kVMDataSymbol,
                       false);
#endif
}

static std::shared_ptr
ResolveIsolateData(const Settings& settings) {
#if OS_IOS
  if (settings.ios_isolate_snapshot_data_path.empty()) {
    return SearchMapping(settings.isolate_snapshot_data,
                         settings.isolate_snapshot_data_path,
                         settings.application_library_path,
                         DartSnapshot::kIsolateDataSymbol,
                         false);
  } else {
    return SetupMapping(settings.ios_isolate_snapshot_data_path);
  }
#else
  return SearchMapping(settings.isolate_snapshot_data,
                       settings.isolate_snapshot_data_path,
                       settings.application_library_path,
                       DartSnapshot::kIsolateDataSymbol,
                       false);
#endif
}

3.4 Flutter.framework / Flutter (engine)

The engine binary ( Flutter ) is relatively large (≈11 M) and offers limited reduction potential. We can trim unused Skia or BoringSSL features (a few hundred kilobytes) and strip debug symbols:

# Strip symbols and generate dSYM
xcrun dsymutil -o $frameworkpath/Flutter.framework.dSYM $frameworkpath/Release/Flutter.framework/flutter
xcrun strip -x -S $frameworkpath/Release/Flutter.framework/flutter

4. Practical Implementation

4.1 Engine Build Configuration

Below are sample build scripts for Debug, Profile and Release modes. They set up the appropriate GN arguments, invoke ninja , and lipo‑merge the resulting frameworks.

# ios_debug.sh (excerpt)
#!/bin/bash
export FLUTTER_SDK="your flutter install path"
# Build simulator
./flutter/tools/gn --unoptimized --runtime-mode debug --simulator
./flutter/tools/gn --unoptimized --ios --runtime-mode debug --simulator
ninja -C out/host_debug_sim_unopt -j 10
ninja -C out/ios_debug_sim_unopt -j 10
# Build armv7
./flutter/tools/gn --unoptimized --runtime-mode debug --ios-cpu arm
./flutter/tools/gn --unoptimized --ios --runtime-mode debug --ios-cpu arm
ninja -C out/host_debug_unopt_arm -j 10
ninja -C out/ios_debug_unopt_arm -j 10
# Build arm64 (similar steps omitted)
# Lipo merge and copy to Flutter SDK cache
lipo -create -output tmp/Flutter.framework/Flutter \
  out/ios_debug_sim_unopt/Flutter.framework/Flutter \
  out/ios_debug_unopt/Flutter.framework/Flutter \
  out/ios_debug_unopt_arm/Flutter.framework/Flutter
cp -rf tmp/Flutter.framework "$FLUTTER_SDK"/bin/cache/artifacts/engine/ios-debug/

4.2 Xcode Engine Debug Configuration

Open the generated products.xcodeproj inside src/out/ios_debug_unopt , drag it into the host Runner.xcworkspace , and add the following environment variables in Generated.xcconfig :

FLUTTER_FRAMEWORK_DIR=/Users/…/engine/src/out/ios_debug_unopt
LOCAL_ENGINE=ios_debug_unopt
FLUTTER_ENGINE=/Users/…/engine/src

4.3 Modifying Engine Sources for Custom Data Paths

We add two new fields to Settings ( ios_vm_snapshot_data_path and ios_isolate_snapshot_data_path ) and fill them in DefaultSettingsForProcess based on resources bundled in the app.

// FlutterDartProject.mm (excerpt)
void initSettings(flutter::Settings& settings) {
#if FLUTTER_RUNTIME_MODE != FLUTTER_RUNTIME_MODE_DEBUG
  // VMData.dat
  NSString* vmDataPath = [[NSBundle mainBundle] pathForResource:@"VMData" ofType:@"dat"];
  if (vmDataPath) settings.ios_vm_snapshot_data_path = vmDataPath.UTF8String;
  // IsolateData.dat
  NSString* isolateDataPath = [[NSBundle mainBundle] pathForResource:@"IsolateData" ofType:@"dat"];
  if (isolateDataPath) settings.ios_isolate_snapshot_data_path = isolateDataPath.UTF8String;
  // icudtl.dat
  NSString* icuPath = [[NSBundle mainBundle] pathForResource:@"icudtl" ofType:@"dat"];
  if (icuPath) settings.icu_data_path = icuPath.UTF8String;
#endif
}

static flutter::Settings DefaultSettingsForProcess(NSBundle* bundle = nil) {
  auto settings = flutter::SettingsFromCommandLine(command_line);
  initSettings(settings);
  return settings;
}

4.4 Flutter‑Side Packaging Script

The script ios_build_reduce.sh performs the following steps:

Build the Flutter iOS framework.

Compile the extracted *.S files into IsolateData.dat and VMData.dat for armv7 and arm64.

Move flutter_assets and icudtl.dat out of the framework.

Package the reduced artifacts into FlutterPackage.zip .

Strip debug symbols from the engine binary and generate a dSYM.

# ios_build_reduce.sh (excerpt)
# Build framework
flutter build ios-framework
# Compile data sections
xcrun cc -arch armv7 -c ./SnapshotData/armv7/IsolateSnapshotData.S -o ./SnapshotData/armv7/HeadIsolateData.dat
xcrun cc -arch armv7 -c ./SnapshotData/armv7/VmSnapshotData.S -o ./SnapshotData/armv7/HeadVMData.dat
tail -c +313 ./SnapshotData/armv7/HeadIsolateData.dat > ./SnapshotData/armv7/IsolateData.dat
tail -c +313 ./SnapshotData/armv7/HeadVMData.dat > ./SnapshotData/armv7/VMData.dat
# Similar steps for arm64 …
# Move assets and icudtl
mv $releasepath/App.framework/flutter_assets $frameworkpath/flutter_reduce/
mv $releasepath/Flutter.framework/icudtl.dat $frameworkpath/flutter_reduce/
# Zip reduced package
zip -r $frameworkpath/FlutterPackage.zip $frameworkpath/flutter_reduce
# Strip engine symbols
xcrun dsymutil -o $frameworkpath/Flutter.framework.dSYM $frameworkpath/Release/Flutter.framework/flutter
xcrun strip -x -S $frameworkpath/Release/Flutter.framework/flutter

4.5 iOS Pod Integration

In the host Podfile we either integrate the Flutter source code or, when IsFlutterSourceCode = 0 , use two podspecs that point to the reduced frameworks:

# Podfile excerpt
IsFlutterSourceCode = 0

def FlutterModuleIntegration
  if IsFlutterSourceCode == 1
    # source integration (official method)
    flutter_application_path = '../native_modules/mlive/'
    load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
    install_all_flutter_pods(flutter_application_path)
  else
    pod 'FlutterDebug', :configurations => ['Debug'], :path => 'LocalLib/Flutter'
    pod 'FlutterRelease', :configurations => ['Release', 'AppStore', 'iAP'], :path => 'LocalLib/Flutter'
  end
end

5. Results

The table below summarizes the size reductions achieved by each optimization:

Name

Optimization Method

Size Savings

icudtl.dat

Download at runtime

≈884 k

flutter_assets

Download at runtime

≈2.1 M

App data segment

Download at runtime

≈2.8 M per architecture

Flutter engine

Strip debug symbols

≈6 M per architecture

Overall framework reduction

≈11.7 M total

By deleting unnecessary files, moving large assets out of the framework, stripping engine symbols, and customizing the engine to load data dynamically, the iOS Flutter integration size is reduced to roughly 10 M.

flutteriOSengine optimizationAOT snapshotdynamic assetspackage size reduction
Tencent Music Tech Team
Written by

Tencent Music Tech Team

Public account of Tencent Music's development team, focusing on technology sharing and communication.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.