Mobile Development 11 min read

Common Flutter Performance Pitfalls and Their Solutions at Soul App

This article details several real‑world Flutter performance problems encountered at Soul—including excessive memory usage for emojis, iOS 14 debug launch restrictions, high CPU consumption of Lottie animations, background‑foreground flickering, and PlatformView memory leaks—and provides concrete mitigation strategies with code examples.

Soul Technical Team
Soul Technical Team
Soul Technical Team
Common Flutter Performance Pitfalls and Their Solutions at Soul App

Flutter has become a popular cross‑platform framework, but its adoption at Soul revealed a series of performance pitfalls that developers need to be aware of.

1. Emoji Rendering Consumes Over 120 MB

Displaying a single Apple Emoji on iOS caused memory usage to jump by more than 120 MB because Flutter does not reuse the native emoji buffer; it allocates a full Sbix table for each emoji.

// font_skia.cc
const size_t table_size = typeface->getTableSize(tag);
if (table_size == 0)
    return nullptr;
void* buffer = malloc(table_size);
if (buffer == nullptr)
    return nullptr;

The team mitigated the issue by using an external Texture to render emojis on the native side, thus reusing the system buffer.

2. iOS 14 Debug Builds Require Xcode Connection

On iOS 14 devices, FlutterEngine fails to start a Debug build because the JIT‑based Dart VM is no longer supported, leading to silent launch failures.

The solution involves ensuring proper Xcode attachment and using the appropriate engine configuration.

3. Lottie Animations Use 30% More CPU Than Native

Flutter’s third‑party Lottie component does not reuse native rendering paths, resulting in noticeably higher CPU usage.

Switching to a native Lottie implementation via PlatformView and setting LottieRendeingEngineCoreAnimation on iOS aligns CPU consumption with native performance.

4. Android Background‑Foreground Switch Causes Screen Flicker

When the app returns from background, mismatched lifecycle events can leave frame scheduling in an incorrect state, producing visual flicker.

// main.dart
PageVisibilityBinding.instance.addGlobalObserver(AppLifecycleObserver());
SoulBinding();
WidgetsFlutterBinding.ensureInitialized();
class SoulBinding extends WidgetsFlutterBinding with BoostFlutterBinding {}

Adjusting FlutterBoost’s lifecycle handling or overriding the default back‑foreground events resolves the issue.

// Disable default handling
FlutterBoostSetupOptions.Builder().shouldOverrideBackForegroundEvent(true).build();
// Manually send events
app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
    // foreground
    FlutterBoost.instance().plugin.onForeground()
    // background
    FlutterBoost.instance().plugin.onBackground()
});

5. PlatformView Leads to Android Activity Memory Leak

Improper disposal of PlatformView objects leaves them retained by PlatformViewsController , causing leaks detected by LeakCanary.

private final MethodChannel.MethodCallHandler parsingHandler = new MethodChannel.MethodCallHandler() {
    @Override
    public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
        // handler may be null after detach, preventing dispose
        if (handler == null) {
            return;
        }
        switch (call.method) {
            case "create":
                create(call, result);
                break;
            case "dispose":
                dispose(call, result);
                break;
            default:
                result.notImplemented();
        }
    }
};

A reflective disposal function that manually invokes the hidden dispose method on the PlatformViewsChannel resolves the leak.

fun dispose(engine: FlutterEngine, id: Int) {
    try {
        val controller = engine.platformViewsController
        val field = controller.javaClass.getDeclaredField("channelHandler")
        field.isAccessible = true
        val obj = field[controller]
        if (obj is PlatformViewsChannel.PlatformViewsHandler) {
            val method = obj.javaClass.getDeclaredMethod("dispose", Int::class.java)
            method.isAccessible = true
            method.invoke(obj, id)
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

6. Conclusion

Despite numerous pitfalls—from memory spikes to CPU overhead and lifecycle quirks—systematic debugging and targeted workarounds enable Flutter to remain a productive cross‑platform solution, and ongoing improvements from the Flutter team continue to reduce these issues.

Debuggingfluttermobile developmentperformanceMemoryplatformview
Soul Technical Team
Written by

Soul Technical Team

Technical practice sharing from Soul

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.