In Dart, when does moving image bytes between isolates as TransferableTypedData beat sending a plain Uint8List, and what rules come with it?
answer
- plain Uint8List is copied on send
- fromList pays the linear copy
- send itself is constant time
- materialize() once, anywhere
- sender loses access after sending
basics
~10 sA Uint8List is mutable, so every send copies it. TransferableTypedData.fromList copies the bytes once, after which sending is constant time and ownership moves: only the receiver can call materialize(), and only once.
solid answer
~50 sA `Uint8List` sent through a `SendPort` is copied during `send()`, on the sending isolate, every time it crosses a port. `TransferableTypedData.fromList(chunks)` copies the bytes once into a transferable buffer; sending that object takes constant time and moves it, so the sender can no longer materialize it and the receiver calls `materialize()` to get a `ByteBuffer`, then `asUint8List()`. `materialize()` is single-use across all isolates. It pays off when bytes cross several ports, when you assemble one buffer from many chunks anyway, or when talking to a `spawnUri` isolate, where it is on the short allowed list. It does not remove the copy, it moves it into `fromList`, so for one hop from a same-group worker the gain can be small. Measure before adopting it, and for a one-shot result prefer `Isolate.exit`, which hands the result over without copying.
code
dart · 15 linesimport 'dart:isolate';
import 'dart:typed_data';
// Worker side: one copy into a transferable, then a constant-time send.
void replyWithImage(SendPort replyTo, int jobId, List<Uint8List> chunks) {
final packed = TransferableTypedData.fromList(chunks);
replyTo.send((jobId, packed)); // the worker can no longer materialize it
}
// Main side: materialize exactly once.
(int, Uint8List) unpack(Object? message) {
final (int id, TransferableTypedData packed) =
message as (int, TransferableTypedData);
return (id, packed.materialize().asUint8List());
}go deeper
Know that sending a Uint8List copies it, and that TransferableTypedData exists to move bytes between isolates more cheaply.
Explain the lifecycle: fromList copies once, send is constant time and moves ownership, and materialize() is single-use and returns a ByteBuffer.
Say where it wins, multi-hop, chunk assembly, spawnUri isolates, long-lived workers, and where it does not, since the copy only moves; prove it with a profile.
Judge whether large-byte traffic between isolates should exist at all, or whether the worker should read and write files itself so only paths and ids cross ports.
## The problem it solves Messages between isolates are copied unless their objects are immutable. A `Uint8List` holding an encoded photo is **mutable**, so sending it copies every byte, and the `SendPort.send` documentation says that copy happens during the call, on the **sending isolate**, in time linear in the data. For a long-lived image-resizing worker, a job's input bytes are copied on the way in and its output bytes on the way out. `TransferableTypedData`, added in Dart 2.4 to speed up moving `Uint8List` data between isolates, changes the cost model from copy-per-send to **copy-once, then move**. ## How it works 1. **Create:** `TransferableTypedData.fromList(List<TypedData> list)` copies the bytes of one or more typed-data objects into a single transferable buffer. This takes time proportional to the number of bytes, and the total must fit in one `Uint8List` on the platform. 2. **Send:** passing the object through a `SendPort`, alone or inside a record, takes **constant time** for its bytes. 3. **Ownership moves:** after the send, the sender's reference can no longer be materialized. The received object is now the only way to get at the data. 4. **Materialize:** the receiver calls `materialize()`, which returns a `ByteBuffer`; `asUint8List()` gives a list view over it. 5. **Once only:** the documentation calls it a cross-isolate **single-use** resource: `materialize()` must not be called more than once on the same bytes, even from different isolates. ## Comparing the options | Approach | Copy cost | Who can use the data afterwards | |---|---|---| | send a `Uint8List` | one copy per send, on the sender | both sides, each with its own copy | | `TransferableTypedData` | one copy in `fromList`; constant-time sends | only the isolate that materializes it | | `Isolate.exit(port, result)` | no copy, the object graph is handed over | the receiver; the worker has ended | ## When it pays off - **Several hops.** Bytes that pass from the UI isolate to a coordinator and on to a worker are copied at every plain send; a transferable is copied once. - **Assembling chunks.** If you would concatenate encoded chunks into one buffer anyway, `fromList` does the concatenation and the packaging in one pass. - **Isolates that do not share code.** `TransferableTypedData` is on the short list of objects a `spawnUri` isolate accepts, where ordinary message passing is also slower. - **Results from a worker that stays alive.** `Isolate.exit` hands over a result without copying only by ending the worker, so a long-lived worker returning large buffers can use a transferable instead. ## When it does not - It does **not** eliminate the copy; it moves it into `fromList`, onto whichever isolate calls it. Sending bytes from the Flutter UI isolate costs a linear copy there either way. - Since Dart 2.15, isolates spawned with `Isolate.spawn` share an isolate group, and the changelog reports much faster inter-isolate communication, so a single same-group hop gains less than the class was designed for. Profile the send before and after. - It adds a rule to your protocol: a transferable is materialized once, so it cannot be fanned out to several consumers or read twice. ## A worked cost comparison Take a 6 MB encoded photo that the UI isolate hands to a coordinator isolate, which forwards it to a resize worker: 1. **Plain `Uint8List`:** the UI isolate copies 6 MB at the first send, and the coordinator copies 6 MB again when it forwards the list. Two linear copies, one of them on the UI isolate. 2. **`TransferableTypedData`:** the UI isolate copies 6 MB once in `fromList`, and both sends are constant time. One linear copy, still on the UI isolate. 3. **Worker reads the file itself:** the UI isolate sends only the file path and a job id. No large copy crosses a port at all. The third option often beats both, which is why the first design question is what actually needs to cross the port. ## Rules to state in an interview - Build it with `fromList`; there is no other public constructor. - Send it once; the sender loses access. - Materialize it once; the result is a `ByteBuffer`. - Keep the id or other metadata in the same record, since the transferable holds only bytes. - Treat it as an optimisation for a measured copy cost, not a default.
- Why not simply return the resized bytes with Isolate.exit?`Isolate.exit` hands a result to the receiver without copying, but it terminates the calling isolate. That suits a one-shot `Isolate.run` job. A long-lived worker has to stay up for the next job, so it cannot use `exit` for each result, and a transferable is one way to keep large replies cheap.
- Can two isolates each materialize the same TransferableTypedData?No. The documentation says `materialize()` must not be called more than once on the same underlying bytes, even when the calls happen in different isolates. If two consumers need the data, materialize once and send each consumer its own copy, or create two transferables.
saying these in an interview costs you the question
- TransferableTypedData avoids copying the bytes at all
- The sender can keep reading the data after sending it
- materialize() can be called again to get a fresh copy
- Sending a Uint8List between isolates shares the same buffer
- It should replace every Uint8List send by default