For most of Flutter's history, the rule was simple: write your app in Dart, ship. The Dart VM is fast, the AOT compiler is excellent, and for 95% of what an app does — UI, network, state, persistence — Dart is the right language. The 5% where Dart is the wrong language is the part that does the heavy lifting: signal processing, image manipulation, ML inference, compression, encryption, anything that touches a tensor or a buffer at 60 frames per second. For those, you call into native code, and for the last three years the native code of choice on the Flutter side has been Rust.
This is what the Flutter + Rust pairing actually looks like in production in 2026 — the tools that work, the tools that do not, the cases where the pairing is the right call, and the cases where the right call is "rewrite this in Dart and stop overthinking it."
Fig. 01 — Two languages, one process. Dart on the UI thread, Rust on the FFI thread, messages crossing the boundary in nanoseconds.
The FFI Boundary, Honestly
Flutter calls into native code through the Dart FFI. The boundary is real, and it is the most important thing to internalize about the pairing. On one side, a Dart object lives in the Dart VM, with the lifetime and memory model that implies. On the other side, a Rust object lives in the system allocator, with the lifetime and memory model that implies. Crossing the boundary means translating between the two memory models, and the cost of the translation is not zero.
The simple case — calling a function that takes a few primitive arguments and returns a primitive result — is essentially free. A few nanoseconds of marshaling, a few nanoseconds of call overhead. You can call into Rust from a hot UI path and never notice. The slightly less simple case — passing a Uint8List from Dart to Rust — is still cheap. The buffer is copied once at the boundary, but the copy is a memcpy and the throughput is in the gigabytes-per-second range.
The expensive case is passing complex Dart objects. A Map<String, dynamic> crossing the FFI boundary requires a full serialization step on the Dart side, a full deserialization step on the Rust side, and a matching teardown. The cost depends on the data, but for non-trivial payloads it is hundreds of microseconds, and for hot paths that is a lot. The rule I follow: keep the FFI boundary at the level of primitives, byte buffers, and opaque pointers. Do the Dart-side work in Dart. Do the Rust-side work in Rust. Pass the minimum data needed to bridge them.
The Tooling That Actually Works in 2026
There are three ways to call Rust from Flutter. Two of them are good. One of them is the future. In order of how much you should care.
dart:ffi directly. This is the lowest level. You write a C header for your Rust library, you use bindgen or cbindgen to generate the FFI bindings, you write the Dart-side wrapper, you manage the lifetimes yourself. It works. It is fast. It is also a lot of code, and the lifetime management is on you. If you have a small, well-defined native module — say, a single signal-processing function — this is the right tool. The boilerplate is bounded, the performance is bounded, and the integration story is straightforward.
flutter_rust_bridge. This is the higher-level wrapper. You write a Rust file with annotated functions, you run the bridge generator, you get a typed Dart API. The generator handles the serialization, the lifetime management, the error handling, and the thread marshaling. The generated code is readable. The error messages are clear. The performance is the same as raw FFI, because the bridge is essentially generating the FFI bindings for you. For most Flutter + Rust projects in 2026, this is the right starting point. It is the highest-leverage tool in the ecosystem.
// rust/src/api.rs
#[flutter_rust_bridge::frb(init)]
pub fn encode_image(bytes: Vec<u8>, quality: u8) -> Result<Vec<u8>, String> {
let img = image::load_from_memory(&bytes)?;
let mut out = Vec::new();
img.write_to(&mut out, ImageFormat::Jpeg)?;
Ok(out)
}
// Dart side, after running `flutter_rust_bridge_codegen`
import 'rust_bridge/api.dart';
final encoded = await RustBridge.encodeImage(rawBytes, 85);
The Rust function is a normal Rust function. The Dart side is a normal Dart function. The bridge is invisible at the call site. You can write 30 of these in an afternoon.
Rust + WebAssembly + wasm_bindgen. This is the future. You compile the Rust to a WebAssembly module, you load it from Dart using the new WASM interop in Dart 3.5+, and you call into it the same way you call into JavaScript. The WASM module runs in the Dart VM's WASM runtime, which is fast, and the interop overhead is even lower than the FFI path. The downside in 2026 is that the WASM interop in Dart is still labeled "experimental" and the tooling is rough. If you are starting a new project, do not bet on it. If you are maintaining a project that is shipping in 2027, watch it.
Where the Pairing Wins
The honest list of where Flutter + Rust earns the interop cost.
Image and video processing. Resizing, cropping, format conversion, thumbnail generation, watermarking, color space conversion. The Rust image crate is faster than any Dart equivalent I have benchmarked, and the interop is straightforward because the inputs and outputs are byte buffers. A 5,000 x 5,000 image resizing to 800 x 800 takes 80 milliseconds in Rust and 350 milliseconds in Dart. The 270 milliseconds you save is the difference between a smooth scroll and a frame stall.
Signal processing and audio. FFTs, convolutions, filters, audio codecs. The Rust rustfft and symphonia crates are state of the art. The Dart equivalents are slower or non-existent. For anything that touches a 44.1 kHz audio stream in real time, Rust is the right tool.
ML inference. ONNX Runtime, Tract, and Candle are all Rust-first ML inference engines. Running an LLM, a vision model, or a speech-to-text model on-device is feasible in Rust in 2026. Doing the same in Dart is theoretically possible and practically miserable. If your app has any kind of on-device AI, the inference will be in Rust and the UI will be in Dart.
Compression and serialization. zstd, lz4, brotli, cap'n proto, flatbuffers. The Rust implementations are uniformly faster and more correct than the Dart ones. For data-heavy apps, moving the compression and serialization into Rust is the difference between a snappy UI and a slow one.
Encryption and security. Ed25519, X25519, ChaCha20, all the primitives that show up in modern cryptography. The Rust implementations are the gold standard. The Dart implementations are "good enough for most uses" but rarely match the Rust versions for performance or constant-time guarantees.
Where the Pairing Does Not Win
The cases where the engineering cost of the FFI boundary is not worth the performance win.
Anything that touches a lot of Dart objects. A function that takes a Map<String, dynamic>, does some work, and returns a Map<String, dynamic> is a bad fit for Rust. The serialization cost is hundreds of microseconds. The performance win is small. The FFI boundary is the wrong tool.
Anything that is just a wrapper around a C library. If the underlying library is already a C library, and you can call it from Dart via dart:ffi directly, do that. Adding a Rust layer to wrap a C library is overhead for no benefit. The cases where Rust wins are when the underlying library is Rust-native.
Anything that you can write in Dart faster than you can write it in Rust. This is a real category. The Dart ecosystem is deep. The Rust ecosystem is deeper in some places and shallower in others. If the operation you need is in package:dio or package:hive and there is no obvious Rust equivalent, do not write a Rust binding for it. Use the Dart package.
Anything where the Dart implementation is already fast enough. This is the most important category. The most common failure mode of the Flutter + Rust pairing is bringing Rust in for a problem where Dart would have been fine. A JSON parse that takes 2 milliseconds in Dart does not need to be 0.5 milliseconds in Rust. The interop cost is real. The performance win needs to be worth it.
The Engineering Practices That Make It Work
If you decide the pairing is right for your use case, the practices that will save you a month of pain.
Start with a single native module. Do not rewrite the whole app in Rust. Pick one operation that is slow, write the Rust implementation, ship it, measure it, and decide whether the FFI overhead is worth the win. If it is, add the next one. If it is not, stop.
Measure the FFI overhead in your benchmark suite. The 10-microsecond FFI call overhead is real. The 100-microsecond serialization overhead is real. The 1-millisecond Rust call that makes everything faster is the goal. Put a benchmark for the Rust function next to a benchmark for the Dart equivalent, and run both in CI. When the Rust benchmark stops being meaningfully faster, the Rust function is no longer earning its keep.
Use flutter_rust_bridge unless you have a reason not to. The lifetime management, the error handling, the thread marshaling, the serialization — all of it is done for you. The performance is the same as raw FFI. The code is shorter. The bug surface is smaller. If you are starting a new project, this is the default.
Keep the boundary at the byte buffer level. Strings in. Bytes out. Numbers in. Numbers out. Avoid passing complex Dart objects. Avoid passing complex Rust structs. The serialization is the slowest part of the cross-FFI call, and the slowest part of the serialization is the recursive walk through collections.
Version the ABI. When you change the Rust function signature, change the version in the bridge. When you change the version, the Dart side will refuse to compile until you regenerate. This is a feature, not a bug. A silent ABI change is the most common source of FFI bugs in production.
When to Skip the Pairing
The list is short and worth taking seriously.
- Your app is a CRUD interface over a REST API. Dart is fast enough. The network is the bottleneck. Rust will not help.
- Your performance problem is in the widget tree, not in compute. Profile first. Most performance problems in Flutter are in the widget tree, not in the Dart VM. Adding Rust will not fix a janky animation.
- You are not bottlenecked on the operation you would move to Rust. Profile first. Move the bottleneck, not the operation that is interesting.
- You do not have time to learn the FFI tooling. The first time you wire up
flutter_rust_bridge, expect a half-day of confusion. The second time, expect two hours. The fifth time, expect ten minutes. If the budget is tight, do not start.
For everything else — image processing, audio, ML, crypto, compression, anything that is a hot loop over bytes — the Flutter + Rust pairing in 2026 is one of the best tools you can pick up. The Rust compiler is the most reliable optimizer in your stack. The FFI boundary is a few nanoseconds when used correctly. The bridge tooling is mature. The ecosystem is deep. The future (WASM) is bright but not here yet. The present is good. Use it.
The Shape of What Comes Next
The cross-language story in mobile is going to keep moving. Today the answer for Flutter + native is Rust over FFI. In two years the answer may be Rust over WASM. In five years the answer may be Dart with an AOT compiler that closes the performance gap with native code. The throughline is the same: use the right tool for the right job, and do not pretend the boundaries do not exist. The boundaries are where the interesting engineering lives.
A small toolkit to get started, if you have not already: install the Rust toolchain via rustup, then cargo install flutter_rust_bridge_codegen. In your Flutter project, add the flutter_rust_bridge Dart package. Write a Rust function. Run the bridge generator. Call it from Dart. The whole loop takes about 15 minutes. The first function you write will be 50% boilerplate and 50% amazement. The tenth will be 5% boilerplate and 95% shipping. That is the loop. Get in it.
Comments
Discuss the article below. Markdown is supported. Sign in with email or GitHub to leave a comment.