Skip to content

Build1 publisher3 min readPublished

Dart's shared_map routes every worker cache read back to the isolate that owns the map

Dart isolates keep separate heaps, so a cross-isolate cache has to be a proxy. package:shared_map makes one instance authoritative and dispatches every get and put to it, so a cache hit costs two messages on that isolate's event loop.

The Engineer · Build desk

What happened

  • Dart gives every isolate its own private heap and its own single-threaded event loop, so a background worker cannot read a cache object that lives in another isolate.
  • A dev.to post on the Dart concurrency model puts the existing options at copying the whole structure across the boundary each time, or hand-rolling a ReceivePort and SendPort server with its own DTOs, correlation IDs and completers.
  • It proposes a third route, package:shared_map by Dart engineer Graciliano M. Passos, described as a synchronized Map built to be shared across isolates and asynchronous workflows.
  • One instance on the primary isolate is authoritative, a sharedReference() token crosses the boundary, and every read or write a worker performs on its proxy is dispatched back to that instance.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint Funnelling every worker read and write through one isolate's event loop puts the isolate you were trying to keep free on the critical path of each cache hit.
  • decision The map's owner has to be an isolate you can afford to interrupt, which makes the post's suggested home on the Flutter UI thread the first choice to question.
  • cost What you stop writing is correlation-ID and completer bookkeeping; what you keep paying is two messages per lookup, at whatever your platform charges for an isolate hop.
  • capability Workers can write entries that other workers then read, without anyone inventing a request/response protocol for a key-value get.

Call `get('user_101')` inside `Isolate.run` and nothing in the worker's own heap answers it. The worker holds a proxy rebuilt from a serialized token [15]. The request crosses back to the isolate that owns the authoritative map, that isolate reads its heap, and the value returns [16]. One request, one response, so two messages per cache hit, both landing on the owner's event loop [19]. Four workers doing a thousand lookups each is 8,000 messages through that one loop [20].

Which isolate owns the map therefore decides what you have built. The post puts the authoritative instance on the Flutter UI thread or the main server loop [13]. In the scenario it opens with, the UI isolate is the one being protected: image resizing and large JSON decoding move to background isolates to hold 120 FPS [3]. Route the cache through that same isolate and every worker lookup becomes UI-thread work. You no longer write the correlation IDs, and you still pay for the messages they used to carry [6].

The API is asynchronous throughout, with `get()`, `put()`, `putIfAbsent()` and `update()` all returning futures [12]. The dev.to post describes the package as a versatile, synchronized `Map` data structure designed specifically to be shared across Dart isolates and asynchronous workflows [8]. Whether `putIfAbsent` settles a race between two workers that miss the same key in the same millisecond is not spelled out in the post. If it does not, the cache removes repeated work across time but not duplicate work inside a burst. Avoiding duplicate work is the motivation the post gives for wanting a shared cache at all [4].

Two of the post's other claims describe the package as published: zero third-party dependencies, and 160 out of 160 pub points on pub.dev with full Dart 3 compatibility [9][10]. Those are packaging and convention scores. Turning them into a latency budget for your app means measuring one `get()` on your target device at your worker count.

The adoption surface is genuinely small. The example reaches a cross-isolate cache with three calls before the boundary (`SharedStore`, `getSharedMap`, `sharedReference`) and one constructor inside the worker [21]. It seeds `user_101` on the main isolate, then has Worker 1 read that key and write `user_102` back into the same store [18]. The alternative the post rejects is a hand-rolled port server with custom request and response DTOs, correlation IDs and response completers, which it calls "150 lines of brittle plumbing just to perform a simple key-value lookup" [6][7].

I would take the proxy where a cached value costs milliseconds to rebuild and each worker reads it a handful of times. Where a worker reads the map inside a tight loop, copying is still cheaper, because serialization is paid once at the handoff and every read after that is local. The post scopes its own objection to copying to large maps and high-frequency operations [5].

What to watch

  • A published round-trip measurement for get() on a mid-range Android device, against the cost of recomputing the cached value, would settle the choice the post argues on ergonomics alone.
  • Documentation on whether putIfAbsent is atomic across isolates, which decides if the cache dedups two workers missing the same key at once.
  • Whether the proxy keeps the same semantics on Web, where Dart's isolate model differs from mobile and server targets.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories