skip to content

In Flutter, what does the compute() function do, and how would you use it to parse a large JSON file without freezing the UI?

level: juniorimportance: must knowfreq 66%

answer

  1. one isolate runs build, layout and your Dart
  2. package:flutter/foundation.dart
  3. a callback plus one message
  4. returns a Future of the result
  5. load the asset first, parse inside

basics

~20 s

compute(callback, message) runs callback(message) on a new background isolate and returns a Future with the result, so heavy CPU work such as jsonDecode plus model mapping no longer blocks the main isolate that builds and schedules frames.

solid answer

~40 s

All of a Flutter app's Dart code, including `build`, layout and your own logic, runs on the **main isolate** by default, so a long synchronous job like decoding a 20 MB JSON file stalls frames. `compute()` from `package:flutter/foundation.dart` takes a callback and a single message, runs `callback(message)` on a freshly spawned isolate and returns a `Future` that completes with the callback's result. You read the file on the main isolate (for example with `rootBundle.loadString`, which is not available in spawned isolates), then `await compute(parseDepartures, body)` where `parseDepartures` is a top-level function doing the decode and the mapping into models. On Android, iOS and desktop it is equivalent to `Isolate.run(() => callback(message))`; while it runs, the UI keeps animating.

code

dart · 29 lines
dart
import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

class Departure {
  const Departure({required this.stopId, required this.time});

  factory Departure.fromJson(Map<String, Object?> json) => Departure(
        stopId: json['stopId']! as String,
        time: json['time']! as String,
      );

  final String stopId;
  final String time;
}

// Top-level: runs on the spawned isolate and captures nothing.
List<Departure> parseDepartures(String body) {
  final List<Object?> raw = jsonDecode(body) as List<Object?>;
  return raw.cast<Map<String, Object?>>().map(Departure.fromJson).toList();
}

Future<List<Departure>> loadSchedule() async {
  // Asset I/O stays on the main isolate.
  final String body = await rootBundle.loadString('assets/schedule.json');
  // The CPU-heavy decode and mapping run on a background isolate.
  return compute(parseDepartures, body, debugLabel: 'parseDepartures');
}

go deeper

for a junior

Know that long synchronous Dart work freezes the UI and that compute(callback, message) moves it to another isolate and returns a Future.

for a middle

Explain what stays on the main isolate (asset loading, setState) and what goes into the callback, and why the callback is top-level.

for a senior

Decide from profiling whether the work is long enough to justify an isolate, and structure the result so it is cheap to hand back.

for a principal

Set a team rule for where parsing and heavy transforms live so screens do not rediscover jank one by one.

## Why heavy parsing freezes a Flutter app A Flutter app runs its Dart code on one **isolate**, the **main isolate**, by default. An isolate is Dart's unit of concurrency: it has its own memory and its own event loop, and it runs one piece of Dart code at a time. On the main isolate that code includes the framework's work for every frame (building widgets, layout, painting instructions) as well as your app logic. If a single synchronous job, such as `jsonDecode` on a 20 MB transit-schedule file followed by turning every entry into a model object, takes several hundred milliseconds, nothing else on the main isolate can run in that time. No frame is produced, animations stop and taps queue up. Marking the function `async` does not change that: the decoding itself is still one long synchronous stretch on the same isolate. ## What compute() does `compute` lives in `package:flutter/foundation.dart`: - **Signature:** `Future<R> compute<M, R>(ComputeCallback<M, R> callback, M message, {String? debugLabel})`, where the callback has the shape `FutureOr<R> Function(M message)`. - **On Android, iOS and desktop** it spawns a new isolate, runs `callback(message)` there, sends the result back and shuts the isolate down. The docs state it is equivalent to `await Isolate.run(() => callback(message))`. - **On the web** it runs the callback on the same event loop, because Dart on the web has no isolates (a separate topic, but worth knowing before relying on it). - **`debugLabel`** names the spawned isolate, so its timeline events are easy to find while profiling. The call returns immediately with a `Future`; the main isolate keeps producing frames while the other isolate works, and the `await` resumes once the result arrives. ## Using it for a large JSON file 1. **Load on the main isolate.** Asset loading through `rootBundle` is tied to the main isolate, so read the file there. Reading is asynchronous I/O and does not block frames. 2. **Put all CPU-heavy steps in one callback.** Decoding, casting and mapping to model classes all belong inside the function you hand to `compute`. 3. **Make the callback a top-level or static function.** It then carries no hidden state to the new isolate, only the message. 4. **Return plain data.** A list of model objects is fine; objects such as a `Future` or an HTTP response object are not good return values. 5. **Update the UI after the await**, checking `mounted` first if you are inside a `State`. ## What stays on the main isolate | Step | Where it runs | |---|---| | `rootBundle.loadString(...)` | main isolate (async I/O) | | `jsonDecode` and `fromJson` mapping | the `compute` isolate | | Showing a progress indicator | main isolate, keeps animating | | `setState` with the parsed list | main isolate | ## Common mistakes - Wrapping the work in `Future(() => ...)` or `async` and expecting it to move off the main isolate. - Calling `compute` for tiny jobs where spawning costs more than the work. - Trying to touch widgets, `BuildContext` or `rootBundle` inside the callback: all UI work belongs to the main isolate.

  • In Flutter, what happens if the compute() callback throws?
    The exception is carried back and the `Future` returned by `compute` completes with that error, so you handle it with `try`/`catch` around the `await` on the main isolate like any other asynchronous failure. The spawned isolate is gone either way.
  • Why can't the compute() callback call rootBundle.loadString itself?
    Flutter's asset bundle and all UI services are tied to the main isolate; the Flutter docs state you cannot access assets through `rootBundle` in spawned isolates. Load the string first, then pass it as the message.

The main isolate is a chef who must also plate every dish on time; compute() hires a prep cook in a separate kitchen, hands over the raw ingredients through a hatch and gets back chopped vegetables, so plating never stops.

saying these in an interview costs you the question

  • Marking the parse function async moves it off the main isolate.
  • compute() runs the callback on a thread that shares the main isolate's memory.
  • You can call setState or read BuildContext inside the compute() callback.
  • compute() blocks the caller until the result is ready.
  • compute() creates a background isolate on the web too.