In Flutter, when does moving a task into compute() fail to pay off, given the cost of spawning an isolate and moving data?
answer
- a fresh isolate on every call
- longer than a few milliseconds
- SchedulerBinding.scheduleTask for tiny jobs
- batch, don't call per item
- I/O waiting is not CPU work
basics
~20 scompute() spawns and tears down a new isolate on every call and moves the message and result across, so it only pays off for CPU work longer than a few milliseconds. Tiny, per-item or I/O-bound tasks gain nothing or get slower.
solid answer
~40 sEvery `compute` call creates a new isolate, sends it the message, runs the callback, sends back the result and exits. That fixed overhead is worth paying when the work is **CPU-bound and long**: the `compute` docs aim it at operations taking longer than a few milliseconds and suggest `SchedulerBinding.scheduleTask` for tasks of up to about a millisecond. It fails to pay off when the job is tiny, when it is called once per item in a loop (50,000 rows means 50,000 spawns), when the input is a huge mutable structure that must be copied, or when the work is really waiting on network or disk, which already does not block the main isolate. Batch the work into one call, pass immutable data like a `String`, and profile before and after.
go deeper
Remember that compute() has a fixed startup cost, so it is for work longer than a few milliseconds, not for every small task.
Explain spawn, send, run and exit per call, how messages and results move, and why batching matters.
Profile before and after an offload, pick batching or a long-lived worker for repeated jobs, and keep inputs immutable.
Decide where heavy data shaping belongs, device, backend or build step, rather than tuning isolates around oversized payloads.
## What each compute() call costs `compute(callback, message)` hides several steps behind one line. On native platforms each call: 1. **Spawns a new isolate** and sets it up. 2. **Sends** the callback and the message to it. 3. **Runs** the callback. 4. **Returns the result** and shuts the isolate down. The Flutter docs on isolates are explicit that short-lived isolates are convenient "but there is performance overhead required to spawn new isolates, and to copy objects from one isolate to another". `compute` pays that overhead on **every** call; nothing is reused between calls. ## How data moves - **Messages.** Mutable objects such as lists and maps are generally copied into the new isolate. The Flutter docs note that immutable objects, such as a `String`, can be passed by reference instead, which makes a large JSON string a cheap message. - **Results.** `compute` returns its result through `Isolate.exit`, which hands the object over without copying it, so a big parsed list is not duplicated on the way back. The expensive case is therefore a large **mutable** input, for example a list of a million maps you already decoded on the main isolate and now want to filter elsewhere. ## When it pays off, and when it does not | Work | Offload with compute()? | Why | |---|---|---| | Decoding and mapping a multi-megabyte JSON string | yes | long CPU work, cheap immutable input | | Resizing or compressing a photo in Dart | yes | long CPU work | | Formatting one date or parsing one small object | no | spawn overhead exceeds the work | | One call per row for 50,000 rows | no | 50,000 spawns; batch into one call | | An HTTP request or a file read | no | waiting on I/O already leaves the main isolate free | | The same heavy job every few seconds | usually not | a long-lived worker isolate avoids repeated spawns | The `compute` docs give the working threshold: useful for operations "that take longer than a few milliseconds", while for tasks taking up to a millisecond `SchedulerBinding.scheduleTask` is suggested instead, so the work runs in a prioritized slot on the main isolate without a spawn. ## Making an offload worth it - **Batch.** One `compute` call over the whole list, not one per element. - **Send immutable input.** Pass the raw `String` and decode inside the callback rather than decoding first and sending the resulting maps. - **Return compact output.** Only what the UI needs, such as the departures for today instead of the full year. - **Reuse for repeated work.** A job that runs over and over is a case for a long-lived isolate with ports rather than `compute`. - **Measure.** In profile mode, compare the main isolate's frame times and the total time to result before and after the change. An offload that makes the UI smooth but the result arrive later may still be the right call; one that changes neither is just overhead. ## Remember the web On the web `compute` does not spawn anything and runs the callback on the same event loop, so none of this overhead analysis applies there, and neither does the benefit.
- Why is passing a raw JSON String to compute() cheaper than passing already-decoded maps?Strings are immutable, and the Flutter docs note that immutable objects can be shared by reference across isolates instead of copied. Decoded maps and lists are mutable, so they are copied object by object into the new isolate. Decoding inside the callback avoids that copy and moves the decode work off the main isolate too.
- In Flutter, what does SchedulerBinding.scheduleTask offer instead of compute() for tiny jobs?It queues a callback on the main isolate with a priority, and the scheduler runs it when the app is not busy with higher-priority work such as animations. There is no isolate to spawn and no data to move, which suits jobs of about a millisecond that must not collide with frames.
saying these in an interview costs you the question
- compute() reuses a pool of warm isolates, so each call is nearly free.
- Every async function should be wrapped in compute() to be safe.
- The result of compute() is always copied back, so returning big lists doubles memory.
- Calling compute() once per list item parallelizes the work efficiently.
- Network requests freeze the UI unless they run inside compute().