Flutter compute: Isolate-Based Background Processing Made Simple
Keep Flutter UIs jank-free by moving heavy work off the main isolate with compute() — how it works, callback rules, when the spawn cost dominates, and when to graduate to long-lived isolates.
Published on • October 29, 2026
AI Assistant

Flutter renders 60 frames a second, which leaves roughly 16 milliseconds per frame to build, layout, and paint. Parse a 20 MB JSON blob on the main isolate in that window and the frame simply doesn’t happen — you see jank, or worse, an “Application Not Responding” dialog. Dart isolates fix this by running code on another core with its own memory, and compute is the shortest path from a blocking function to a background one.
In this tutorial, you will learn what compute actually does under the hood, the rules its callback must satisfy, how to use it for JSON parsing and similar work, when isolate-spawn overhead outweighs the benefit, and when to graduate to long-lived isolates.
Why the main isolate blocks
All Dart code runs in isolates, and by default a Flutter app does everything on a single one. As Flutter’s concurrency docs explain, for smooth rendering the engine adds a paint-frame event to the event queue 60 times per second, and “whenever a process can’t be completed in a frame gap… it’s a good idea to offload the work to another isolate.” Common candidates they list: reading data from a local database, parsing and decoding large data files, processing or compressing photos, audio, and video, and filtering complex lists.
Isolates communicate only by message passing. Messages are generally copied from sender to receiver, with immutable objects like strings sent by reference for performance — and, importantly, Isolate.run and compute use Isolate.exit under the hood, which transfers ownership of the result rather than deep-copying it.
The compute API
The signature is small:
Future<R> compute<M, R>(
ComputeCallback<M, R> callback,
M message, {
String? debugLabel,
})
It “asynchronously runs the given callback — with the provided message — in the background and completes with the result,” and is explicitly “useful for operations that take longer than a few milliseconds, and which would therefore risk skipping frames.” The optional debugLabel names the isolate in Timeline events, which is handy when profiling in DevTools.
On native platforms, await compute(fun, message) is equivalent to await Isolate.run(() => fun(message)): spawn an isolate, run the function, transfer the result back, terminate. On the web, where multiple isolates aren’t supported, compute runs the callback on the current event loop instead — so your code compiles and still runs everywhere.
A real example: parsing JSON off the UI thread
The classic case is a large JSON payload being converted into Dart objects:
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart' show rootBundle;
Future<List<Photo>> loadPhotos() async {
final String jsonString = await rootBundle.loadString('assets/photos.json');
// Off the UI isolate: decoding ~20MB here won't drop frames.
return compute(parsePhotos, jsonString);
}
List<Photo> parsePhotos(String jsonString) {
final List<Object?> data = jsonDecode(jsonString) as List<Object?>;
return data
.cast<Map<String, Object?>>()
.map(Photo.fromJson)
.toList();
}
The callback here is a plain top-level function taking a single argument and returning the decoded result — exactly the shape compute wants.
Callback rules: keep it top-level (or static)
The docs are explicit about what crosses the isolate boundary: “The callback, the message given to it as well as the result have to be objects that can be sent across isolates (as they may be transitively copied if needed).” They also link to SendPort.send with “a note of warning about sending closures, which can capture more state than needed.”
That warning is the practical rule. A closure passed to compute has its captured variables serialized along with it — including that giant list or database handle you didn’t intend to send. The reliable pattern is a top-level or static function with a single, cheap parameter:
// Good: only the string crosses the boundary.
Future<Result> process(String input) => compute(_processInput, input);
Result _processInput(String input) {
// heavy, self-contained work
return Result.fromJson(input);
}
Return the same way: prefer plain data (maps, lists, strings, your serializable models) over live objects. No BuildContext, no plugins tied to the UI, no open resources.
When compute is (and isn’t) worth it
Isolate spawning isn’t free — there’s startup cost plus message transfer. The Flutter docs’ guidance is that the hard rule is only this: use isolates “when large computations are causing your Flutter application to experience UI jank,” meaning work that takes longer than a frame gap.
Rough heuristics that hold up in practice:
- Worth it: parsing multi-megabyte JSON, image decode/encode, encryption of sizable payloads, heavy regex or search over big lists — anything measured in tens of milliseconds or more.
- Not worth it: sub-millisecond work. For tasks that take up to about a millisecond, the
computedocs suggestSchedulerBinding.scheduleTaskinstead. Also not worth it for one-off tiny transforms — the spawn will cost more than the work. - Measure first. Wrap suspicious code in a stopwatch, check the DevTools performance overlay, and only then reach for
compute. A function that’s fast for 100 records may be jank-inducing for 100,000.
One more platform note: since web runs compute callbacks on the main thread, isolates won’t rescue a janky Flutter web app — they buy you portability and code-sharing, not web-side parallelism.
Graduating to long-lived isolates
If you’re calling compute repeatedly for the same kind of work — a photo editor applying filters, a client continuously decoding streams — the per-call spawn overhead adds up. Dart’s own guidance says: “if your code relies on repeatedly running the same computation using Isolate.run, you might improve performance by instead creating long-lived isolates that don’t exit immediately.”
That means dropping to the lower-level primitives compute abstracts:
Isolate.spawn()andIsolate.exit()ReceivePortandSendPortfor two-way messagingSendPort.send()to post messages
The shape is a worker class: spawn once, send jobs as messages (ideally tagged with an ID and paired with a Completer so responses route to the right caller), and close the ports when done. There are also packages like worker_manager that manage pools for you.
Two isolate limitations from the Flutter docs are worth remembering as you go deeper: spawned isolates can’t access rootBundle or dart:ui (all UI work stays on the main isolate), and background isolates can’t receive unsolicited platform messages — you can query a plugin for a response in the background, but you can’t host a long-lived push listener there.
Practical takeaways
compute(callback, message)spawns a short-lived isolate, runs your work, transfers the result back (viaIsolate.exit), and terminates — equivalent toIsolate.runon native, main-thread on web.- Use it for work longer than a frame gap: big JSON parsing, image/media processing, encryption, heavy filtering. Skip it for sub-millisecond tasks (
SchedulerBinding.scheduleTaskcovers those). - Pass a top-level or static callback with a single cheap parameter; closures drag their captured state across the isolate boundary.
- Send and return plain, sendable data — no context, no UI objects, no shared mutable state.
- Reaching for
computein a hot loop? Build a long-lived worker isolate withIsolate.spawn+ReceivePort/SendPortinstead.
Conclusion & Next Steps
compute is the one-line insurance policy against dropped frames: a documented, cross-platform wrapper around short-lived isolates with clear rules about what crosses the boundary. Start by measuring, move only the genuinely heavy calls, and keep the callbacks boring. Next, read Dart’s isolates guide for the robust ports pattern with error handling and shutdown, and explore BackgroundIsolateBinaryMessenger if your background work needs platform plugins.
Sources
- Flutter API — compute function. https://api.flutter.dev/flutter/foundation/compute.html
- Flutter — Concurrency and isolates. https://docs.flutter.dev/perf/isolates
- Dart — Isolates. https://dart.dev/language/isolates