A Simpler Way to Boost Flutter Performance with Dart FFI

Most of the time Dart is fast enough. The JIT compiler is great for development and the AOT compiler produces efficient native code for release builds. But sometimes you hit a wall. You have a function that does heavy numerical computation or image processing and it’s just not fast enough. It makes your UI jank.

The common advice is to move the work to a background isolate. This is a good first step and we talked about it in Keeping Your Flutter UI Smooth with Isolates. But what if the work itself is the bottleneck? You could also use platform channels to call native code as we covered in Calling Native Code from Flutter Is Easier Than You Think. This works well for accessing platform APIs like the camera or battery level.

But platform channels have overhead. You have to serialize your data send it over a channel deserialize it on the other side and do the same for the response. This is fine for infrequent calls but for performance critical loops it adds up.

There is a more direct way. Dart has a foreign function interface or FFI. It lets you call C functions directly from Dart code as if they were native Dart functions. There is no channel and no serialization. It’s just a direct function call. This is your tool for when you need maximum performance.

When to Use FFI

FFI is not a replacement for platform channels. They solve different problems. You should reach for FFI in a few specific situations.

First is for pure CPU intensive computation. Think video encoding cryptography or a physics engine. These tasks are often already implemented in highly optimized C or C++ libraries. FFI lets you use those libraries directly instead of rewriting them in Dart.

Second is when you need to reuse an existing native codebase. Many companies have valuable libraries written in C. FFI lets you bring that code into your Flutter app without a major rewrite.

Third is for low level access to the operating system or hardware that isn’t exposed through a high level platform API. FFI lets you make system calls directly.

For anything that involves platform specific UI or services like notifications or in app purchases platform channels are still the right tool.

A Simple Example

Let’s start with something simple. We will call a C function that adds two numbers. First we need the C code. Create a file named native_add.c.

// native_add.c
int native_add(int a, int b) {
    return a + b;
}

Now we compile this into a shared library. The command depends on your operating system.

On macOS you would run this.

# For macOS
gcc -shared -o libnative_add.dylib native_add.c

On Linux you would run this.

# For Linux
gcc -shared -fPIC -o libnative_add.so native_add.c

This creates a file libnative_add.dylib or libnative_add.so. Now let’s call it from Dart.

// main.dart
import 'dart:ffi';
import 'dart:io' show Platform;

// Define the function signature in C.
typedef NativeAdd = Int32 Function(Int32 a, Int32 b);
// Define the function signature in Dart.
typedef DartAdd = int Function(int a, int b);

void main() {
  // Open the dynamic library.
  var path = 'libnative_add.so'; // For Linux
  if (Platform.isMacOS) {
    path = 'libnative_add.dylib';
  }
  final dylib = DynamicLibrary.open(path);

  // Look up the C function 'native_add'.
  final nativeAdd = dylib.lookup<NativeFunction<NativeAdd>>('native_add');

  // Cast it to a Dart function.
  final add = nativeAdd.asFunction<DartAdd>();

  // Call the function.
  final result = add(10, 20);
  print('The result is $result'); // Prints: The result is 30
}

There are three steps. First you open the shared library. Second you look up the symbol for the function you want to call. Third you cast that symbol to a Dart function type making sure the C and Dart signatures match. Then you can just call it. It’s that direct.

Handling Pointers and Structs

Real world C libraries use pointers and structs. FFI can handle these too. Let’s say you have a C function that processes a coordinate struct.

Here is the C code in native_lib.c.

// native_lib.c
#include <stdint.h>

struct Coordinate {
    double x;
    double y;
};

void process_coordinate(struct Coordinate* coord) {
    // Example processing: double the values
    coord->x = coord->x * 2;
    coord->y = coord->y * 2;
}

And here is the Dart code to call it. This requires the package:ffi which provides helpers for memory management.

import 'dart:ffi';
import 'package:ffi/ffi.dart';

// Define the C struct as a Dart class.
final class Coordinate extends Struct {
  @Double()
  external double x;

  @Double()
  external double y;
}

// Define the C function signature.
typedef ProcessCoordinate = Void Function(Pointer<Coordinate> coord);
// Define the Dart function signature.
typedef DartProcessCoordinate = void Function(Pointer<Coordinate> coord);

void main() {
  // This assumes you have compiled native_lib.c into a shared library.
  final dylib = DynamicLibrary.open('libnative_lib.so');

  final processCoordinate = dylib
      .lookup<NativeFunction<ProcessCoordinate>>('process_coordinate')
      .asFunction<DartProcessCoordinate>();

  // Allocate memory for the struct.
  final pointer = calloc<Coordinate>();

  // Set values on the struct.
  pointer.ref.x = 10.5;
  pointer.ref.y = 20.5;

  print('Before: (${pointer.ref.x}, ${pointer.ref.y})');

  // Call the C function passing the pointer.
  processCoordinate(pointer);

  print('After: (${pointer.ref.x}, ${pointer.ref.y})');

  // IMPORTANT: Free the memory you allocated.
  calloc.free(pointer);
}

You map the C struct to a Dart class that extends Struct. The key part is that you manage the memory yourself. You allocate memory for the struct with calloc get a pointer and pass that pointer to the C function. When you are done you must free the memory. This is the tradeoff for raw performance. You get C-like power and you also get C-like responsibility for memory.

Bundling the shared library with your Flutter app requires a few platform specific steps in your android and ios folders but it is a well defined process. For production apps you would typically use a tool like ffigen to automatically generate the Dart bindings from your C header files which reduces boilerplate and errors.

FFI is not something you will use every day. But when you hit a performance ceiling it’s an incredibly powerful tool. It lets you drop down to the metal and squeeze every bit of performance out of the hardware without leaving the Dart ecosystem.

Think about the slowest part of your application. Is it just waiting or is it actually computing? Now try to answer the question for yourself.

— Rishi Banerjee
September 2025