Flutter uygulamanda büyük bir JSON dosyasını parse ederken veya bir görseli işlerken arayüzün bir anlığına donduğunu fark ettiysen, suçlu genelde tek bir şeydir: Dart isolate compute kullanımı eksik. Dart, her uygulamayı tek bir thread üzerinde event loop koşturan bir isolate içinde çalıştırır; bu yazıda compute(), Isolate.run() ve elle kurulan kalıcı isolate'lerle ağır işleri UI thread'inden nasıl ayıracağını, ne zaman isolate'e ihtiyacın olduğunu ve neyin isolate'ler arasında gönderilip gönderilemeyeceğini adım adım göreceksin.
💡 Pro Tip:compute()ileIsolate.run()native platformlarda birebir aynı mekanizmayı çalıştırır — aralarında seçim yapmıyorsun, yalnızca "Flutter API'si mi, saf Dart API'si mi" sorusuna cevap veriyorsun.
İçindekiler
- Dart'ın Tek-Thread Modeli ve Event Loop
- Neden mutex/lock yok?
- Isolate: Paylaşılan Bellek Yok, Mesaj Var
- compute() ile Tek Atımlık İş
- Isolate.run() ve Uzun Ömürlü Isolate
- Kalıcı isolate'e ne zaman geçmeli?
- SendPort/ReceivePort ile Çift Yönlü İletişim
- Ne Gönderilebilir, Ne Gönderilemez: TransferableTypedData
- Ölçüm: DevTools Timeline'da Jank'i Görmek
- Isolate'in Maliyeti — Ne Zaman KULLANMA
- Pratik Örnekler: JSON Parse, Görsel İşleme, Şifreleme
- SSS
- Dart isolate nedir ve thread'den farkı nedir?
- Flutter'da compute() ne zaman kullanılır?
- Isolate'ler arasında nasıl veri paylaşılır?
- JSON parse işlemi neden UI'yi dondurur?
- Isolate.run() ile compute() arasında hangisini seçmeliyim?
- Kalıcı worker isolate kurmak ne zaman fazla mühendislik olur?
- Güncelleme (Eylül 2026)
- Sonuç
- Kaynaklar
Dart'ın Tek-Thread Modeli ve Event Loop
Dart, her uygulamayı en az bir "isolate" içinde çalıştırır. Resmi dil rehberi isolate'i şöyle tanımlıyor: her isolate kendi belleğine ve event loop koşturan tek bir thread'e sahiptir. Bu, JavaScript'in tek-thread event-loop modeline benzer, ama Dart'ta paralel iş için ek isolate'ler açılabilir — Node.js worker_threads veya Web Worker'a yakın bir model.
Flutter'da UI, tek bir "main isolate" (UI thread) üzerinde koşar. Bu isolate hem senin uygulama kodunu hem de Flutter framework kodunu (widget build, layout, paint) çalıştırır. DevTools performans rehberi bunu açıkça vurgular: UI thread Dart VM içinde Dart kodu çalıştırır ve bu thread bloklanmamalıdır. Ağır bir senkron işlem (büyük JSON parse, sıkıştırma, şifreleme) bu tek thread'de çalıştığında, event loop'taki diğer olaylar — frame üretimi dahil — sıraya girer ve gecikir. Bunun görünür sonucu "jank"tır: arayüzün donduğu, kaydırmanın takıldığı anlar.
Burada anlaşılması gereken kritik nokta şu: event loop tek başına "yavaş" değildir — asenkron I/O (ağ isteği, dosya okuma, Future/Stream tabanlı beklemeler) event loop'u hiç bloklamaz, çünkü bu işler zaten bekleme sırasında thread'i boşta bırakır. Sorun yalnızca SENKRON ve CPU-yoğun kodda ortaya çıkar: for döngüsü, jsonDecode, sıkıştırma veya şifreleme algoritması gibi, işlemci zamanını gerçekten tüketen ve await ile bölünemeyen işler. İşte tam da bu noktada isolate devreye girer — asenkron beklemeyi değil, CPU'yu gerçekten meşgul eden hesaplamayı ana thread'den uzaklaştırmak için.
Neden mutex/lock yok?
Dart'ta klasik eşzamanlılık araçlarına (mutex, lock, semafor) ihtiyaç yoktur, çünkü model bunları gereksiz kılacak şekilde tasarlanmıştır — bir sonraki bölümde bunun nedenini göreceksin.
Isolate: Paylaşılan Bellek Yok, Mesaj Var
Kritik fark izolasyonun derecesinde: bir isolate'in global alanları başka hiçbir isolate'ten erişilemez. Bu tasarım bilinçli bir tercih — paylaşılan bellek olmadığı için mutex, lock, semafor gibi klasik eşzamanlılık araçlarına ve bunların getirdiği veri yarışı (data race) hatalarına Dart'ta ihtiyaç kalmaz. "Isolate" adının kendisi de buradan geliyor: birbirinin belleğini "göremezler".
Isolate'ler arası TEK iletişim yolu mesaj geçişidir; ReceivePort ve SendPort çifti bunu sağlar. Bir SendPort her zaman tam olarak bir ReceivePort'a bağlıyken, bir ReceivePort'un birden çok SendPort'u olabilir — yani "çoğa-bir" bir mesaj kanalı yapısı. Bu, bir worker isolate'in birden fazla kaynaktan mesaj alabileceği ama yanıtlarını tek, belirli bir hatta geri gönderdiği anlamına gelir.
- Isolate: kendi belleği + kendi event loop'u olan izole çalışma birimi.
- SendPort: bir isolate'e mesaj göndermeye yarayan uç.
- ReceivePort: mesajları dinleyen,
Streamgibi davranan uç.
"Çoğa-bir" kanal yapısının pratikte anlamı şudur: bir worker isolate kurduğunda, o worker'ın tek bir ReceivePort'u birden fazla ekranın veya widget'ın gönderdiği komutları aynı anda dinleyebilir. Bu, tek bir kalıcı isolate'i basit bir "iş kuyruğu" gibi kullanmanı mümkün kılar: farklı yerlerden gelen istekler aynı SendPort'a değil, aynı ReceivePort'a bağlı farklı SendPort'lara gönderilir, worker da bunları sırayla işler.
compute() ile Tek Atımlık İş
Flutter/Dart ekibi, çoğu "tek atımlık ağır iş" senaryosu için elle SendPort/ReceivePort kurmak yerine üst seviye iki API sunar: Isolate.run() ve Flutter'ın compute() fonksiyonu.
Flutter tarafında compute(), tam olarak bu deseni sarmalar: Future<R> compute<M, R>(ComputeCallback<M, R> callback, M message, {String? debugLabel}) imzasıyla, callback'i arka planda çalıştırıp sonuçla tamamlanan bir Future döner. Native platformlarda bu, birebir await Isolate.run(() => fun(message)) ile eşdeğerdir; yalnızca web'de (isolate'lerin desteklenmediği yerde) callback mevcut event loop'ta çalışır. compute() ayrıca bir alt sınır da koyar: yalnızca birkaç milisaniyeyi aşan işler içindir; en fazla bir milisaniye süren işler için SchedulerBinding.scheduleTask önerilir — her küçük senkron hesap için isolate açmak, spawn maliyeti yüzünden tam tersi etkiye (daha fazla gecikme) yol açabilir.
dart
1// Büyük bir JSON string'ini UI thread'ini bloklamadan decode et.2Future<List<Photo>> parsePhotosInBackground(String jsonBody) {3 return compute(_parsePhotos, jsonBody, debugLabel: 'parsePhotos');4}5 6// Üst düzey (top-level) veya static bir fonksiyon olmalı.7List<Photo> _parsePhotos(String jsonBody) {8 final List<dynamic> parsed = jsonDecode(jsonBody) as List<dynamic>;9 return parsed10 .map<Photo>((dynamic json) => Photo.fromJson(json as Map<String, dynamic>))11 .toList();12}Isolate.run() ve Uzun Ömürlü Isolate
Isolate.run(), worker isolate açma-kapama adımlarını (spawn, sonucu bekleme, isolate'i kapatma) tek bir çağrıda sadeleştirir. Sonucu her zaman bir Future olarak döner çünkü ana isolate bu bekleme sırasında çalışmaya devam eder — yani API tamamen asenkron ve await ile kullanılır, isolate'in kendisiyle SendPort seviyesinde uğraşmaya gerek kalmaz.
dart
1Future<List<Photo>> parsePhotosWithIsolateRun(String jsonBody) async {2 return Isolate.run(() {3 final List<dynamic> parsed = jsonDecode(jsonBody) as List<dynamic>;4 return parsed5 .map<Photo>((dynamic j) => Photo.fromJson(j as Map<String, dynamic>))6 .toList();7 });8}Hem compute() hem Isolate.run() "kısa ömürlü" (short-lived) isolate deseni kurar: isolate açılır, iş yapılır, sonuç dönülür, isolate kapanır. Bu kullanışlıdır ama bedelsiz değildir: yeni isolate açmanın ve nesneleri bir isolate'ten diğerine kopyalamanın performans yükü vardır. Aynı hesaplama tekrar tekrar Isolate.run ile yapılıyorsa (örneğin her kullanıcı etkileşiminde ayrı bir kısa ömürlü isolate açılıyorsa), tekrar açılıp kapanmayan, mesajla iş alan KALICI bir isolate kurmak daha performanslı olabilir.
Yaklaşım | Ömür | Kurulum | En uygun senaryo |
|---|---|---|---|
compute() | Kısa (tek atımlık) | Otomatik | Flutter kodu, web dahil çapraz platform |
Isolate.run() | Kısa (tek atımlık) | Otomatik | Saf Dart kodu, tek çağrı |
Kalıcı worker isolate | Uzun | Elle (SendPort/ReceivePort) | Sık tekrarlanan/aynı türde ardışık işler |
Kalıcı isolate'e ne zaman geçmeli?
Resmi dil rehberi burada net bir işaret verir: aynı hesaplamayı tekrar tekrar Isolate.run ile yapıyorsan, hemen çıkmayan isolate'ler kurarak daha iyi performans elde edebilirsin. Örneğin bir arama kutusu her tuş vuruşunda compute() ile büyük bir listeyi yeniden filtreliyorsa, her karakterde yeni bir isolate açılıp kapanır — bu durumda arama mantığını kalıcı bir worker isolate'e taşıyıp yalnızca mesaj göndermek, tekrarlanan spawn maliyetini ortadan kaldırır. Tek seferlik bir dosya import işleminde ise bu karmaşıklığa gerek yoktur; compute() yeterlidir.
SendPort/ReceivePort ile Çift Yönlü İletişim
Çift yönlü iletişim ihtiyacı — worker'a birden fazla komut gönderip birden fazla yanıt almak — tam olarak Isolate.run'ın kapsamadığı senaryodur; burada devreye kalıcı isolate ve kendi ReceivePort/SendPort çifti girer: ana isolate bir ReceivePort açar, worker'a bu portun sendPort'unu gönderir, worker de kendi ReceivePort'unu açıp SendPort'unu ana tarafa göndererek iki yönlü bir kanal kurar. Bir ReceivePort birden çok SendPort'tan mesaj alabildiği için, tek bir worker isolate birden fazla "istemciden" komut dinleyebilir.
dart
1Future<SendPort> spawnWorker() async {2 final ReceivePort initPort = ReceivePort();3 await Isolate.spawn(_workerEntry, initPort.sendPort);4 // Worker ilk mesaj olarak KENDİ SendPort'unu gönderir.5 final SendPort workerPort = await initPort.first as SendPort;6 initPort.close();7 return workerPort;8}9 10void _workerEntry(SendPort mainSendPort) {11 final ReceivePort workerPort = ReceivePort();12 mainSendPort.send(workerPort.sendPort);13 workerPort.listen((dynamic message) {14 final int input = message as int;15 mainSendPort.send(input * input);16 });17}Ne Gönderilebilir, Ne Gönderilemez: TransferableTypedData
Isolate'ler arası mesajlaşmanın en sık atlanan detayı, HER Dart nesnesinin gönderilemeyeceğidir. Resmi dokümantasyon, native kaynak tutan nesnelerin (ör. Socket) ile ReceivePort, DynamicLibrary, Finalizable, Finalizer, NativeFinalizer, Pointer, UserTag nesnelerinin ve @pragma('vm:isolate-unsendable') ile işaretlenmiş sınıf örneklerinin gönderilemeyeceğini açıkça listeler. Bunun teknik nedeni basit: bu nesneler işletim sistemi seviyesinde kaynaklara (dosya tanıtıcısı, native bellek işaretçisi, native thread) referans tutar; bu referanslar bir isolate'e kopyalanamaz veya taşınamaz, çünkü kaynağın kendisi belirli bir işletim sistemi/VM bağlamına bağlıdır. Isolate.spawn() ve Isolate.exit() da aynı SendPort mekanizmasını kullandığı için aynı kısıtlara tabidir.
Gönderilebilir | Gönderilemez |
|---|---|
int, double, String, bool, null | Socket |
List, Map, Set (gönderilebilir elemanlardan oluşuyorsa) | ReceivePort |
SendPort | DynamicLibrary |
TransferableTypedData | Pointer, Finalizable, Finalizer, NativeFinalizer, UserTag |
Büyük ikili veri (ör. bir görüntünün ham byte dizisi) taşımanın normal yolu KOPYALAMAKTIR ve byte sayısıyla orantılı sürede sürer. TransferableTypedData, bu maliyeti ortadan kaldırmak için var: byte dizisini sabit zamanlı, kopyasız şekilde bir isolate'ten diğerine "taşır". Bunun bedeli tek kullanımlıktır: gönderen taraf transfer sonrası veriyi bir daha materialize() edemez; veriyi somut bir ByteBuffer'a çevirmenin tek yolu artık ALICI taraf olur. Bu, C'deki "move semantics"e benzer bir model — paylaşım değil, sahiplik devri.
Bunun neden önemli olduğunu somutlaştıralım: normal bir Uint8List'i mesaj olarak gönderdiğinde, Dart runtime'ı byte'ları gönderen isolate'in belleğinden alıcı isolate'in belleğine tek tek kopyalar — veri büyüdükçe bu kopyalama süresi de büyür. TransferableTypedData ise belleği kopyalamaz, sahipliğini devreder; GÖNDERİM adımı sabit zamanlıdır, ama TransferableTypedData.fromList ile paketleme adımı resmi dokümana göre byte sayısıyla orantılı sürer — kazanç, isolate sınırında kopyalamanın ortadan kalkmasındadır. Pratik kural şu: birkaç kilobaytlık küçük bir yapılandırma nesnesi için TransferableTypedData kullanmaya gerek yok, ama bir fotoğrafın ya da ses dosyasının ham byte'larını isolate'ler arasında taşıyorsan bu sarmalayıcı doğrudan performans farkı yaratır.
dart
1void sendImageBytes(SendPort target, Uint8List rawBytes) {2 final TransferableTypedData packet = TransferableTypedData.fromList(<Uint8List>[rawBytes]);3 target.send(packet);4}5 6void receiveImageBytes(TransferableTypedData packet) {7 final Uint8List bytes = packet.materialize().asUint8List();8 // bytes artık bu isolate'in sahipliğinde.9}Ölçüm: DevTools Timeline'da Jank'i Görmek
Isolate kullanma kararı, teoriden değil ölçümden çıkmalı. Flutter'ın resmi performans rehberi burada net bir eşik verir: 60Hz bir cihazda bir frame ~16ms'yi aşarsa "janky" sayılır — bu, saniyede 60 kare hedefinin doğrudan matematiksel sonucudur (1000ms / 60 ≈ 16,6ms). DevTools'un Performance/Timeline görünümü, frame render grafiğinde jank yaşayan kareleri kırmızı bir overlay ile işaretler; bir janky frame seçildiğinde "Frame Analysis" sekmesi, o karede neyin pahalı olduğuna dair debug ipuçları gösterir.
Ölçümün geçerli olması için bir ön koşul var: analiz profile build ile yapılmalı; debug modda ölçülen frame süreleri release performansını yansıtmaz. Yani "isolate'e taşıyayım mı" sorusuna cevap ararken debug modda gözlemlenen bir takılma yanıltıcı olabilir — asıl karar flutter run --profile ile (veya DevTools'a profile build ile bağlanarak) alınan ölçüme dayanmalı.
Pratikte akış şöyle işler: uygulamayı profile modda çalıştırıp DevTools'un Performance sekmesini açarsın, jank'e neden olduğunu düşündüğün etkileşimi (liste kaydırma, dosya açma, arama yapma) tekrarlarsın, ardından frame render grafiğinde kırmızı overlay ile işaretli kareleri ararsın. Böyle bir kare bulduğunda, Frame Analysis sekmesindeki debug ipuçlarını okuyarak darboğazın UI thread'de çalışan senkron Dart kodundan mı kaynaklandığını doğrularsın — eğer öyleyse, bu iş isolate'e taşınabilecek somut bir adaydır.
Isolate'in Maliyeti — Ne Zaman KULLANMA
Flutter ekibi, isolate kullanımı için TEK kesin kuralı da bu ölçüme bağlar: büyük bir hesaplama gerçekten UI jank'ine yol açtığında isolate'e taşınmalı — bunun dışında "önden optimizasyon" olarak her işi isolate'e atmak gerekmez. Kısa süren bir hesap için isolate açmak, spawn ve mesaj kopyalama maliyeti yüzünden net gecikmeyi ARTIRABİLİR; compute() bu yüzden "birkaç milisaniyeyi aşan işler" için önerilir, en fazla bir milisaniye süren işler için SchedulerBinding.scheduleTask önerilir.
- Kullan: tekrarlanmayan, büyük ve senkron bir hesaplama UI jank'ine yol açıyorsa.
- Kullanma: iş zaten birkaç milisaniyenin altındaysa (spawn maliyeti işin kendisinden büyük olur).
- Kalıcı isolate'e geç: aynı ağır iş sık sık, kısa aralıklarla tekrarlanıyorsa.
Bu üç maddeyi tek bir cümlede özetlemek gerekirse: isolate bedava değildir, bu yüzden "her ihtimale karşı isolate'e sar" refleksi yanlış bir güvenlik hissi verir. Ölçüm yapmadan isolate eklemek, hem kod karmaşıklığını (asenkron akış, mesaj serileştirme, hata yönetimi) hem de küçük işlerde net gecikmeyi artırabilir; bu yüzden sıralama her zaman önce ölç, sonra taşı olmalı.
Pratik Örnekler: JSON Parse, Görsel İşleme, Şifreleme
Tipik olarak jank'e yol açan ve isolate'e taşınması önerilen işler: yerel veritabanından veri okuma, büyük veri dosyalarını ayrıştırma/decode etme, fotoğraf/ses/video dosyalarını işleme veya sıkıştırma, ses/video dönüştürme. JSON parse, görsel işleme ve şifreleme, bu kategorinin en sık karşılaşılan somut örnekleridir:
- JSON parse: API'den gelen büyük bir liste yanıtını
compute()içindejsonDecode+ model dönüşümüyle işle (yukarıdakiparsePhotosInBackgroundörneği). Liste binlerce öğe içeriyorsa, decode + model dönüşümünün ikisi de aynıcompute()çağrısının içinde kalmalı; yalnızcajsonDecode'u isolate'e taşıyıp model dönüşümünü ana isolate'te yapmak, işin ağır kısmını yine UI thread'inde bırakır. - Görsel işleme: bir görselin ham byte'larını
TransferableTypedDataile worker isolate'e taşı, orada yeniden boyutlandırma/filtre uygula, sonucu yineTransferableTypedDataile geri gönder. Bir galeri ekranında aynı anda birden fazla görsel işleniyorsa, her görsel için ayrıcompute()açmak yerine tek bir kalıcı worker isolate'e sırayla iş göndermek, tekrarlanan spawn maliyetinden kaçınır. - Şifreleme: büyük bir dosyanın AES gibi bir algoritmayla şifrelenmesi CPU-yoğun ve senkron bir işlemdir; tek atımlıksa
compute(), kullanıcı sık sık dosya şifreliyorsa kalıcı worker isolate uygundur. Şifreleme anahtarı gibi hassas verileri worker'a gönderirken de aynı gönderilebilirlik kurallarına tabi olduğunu unutma — anahtarıString/Uint8Listgibi gönderilebilir bir tür olarak taşı, native bir kriptografi kütüphanesinin tuttuğu birPointer'ı asla mesaj olarak göndermeye çalışma.
Ben genelde önce compute()/Isolate.run() ile başlamayı, yalnızca DevTools'ta tekrarlanan spawn maliyetini gördüğümde kalıcı worker isolate'e geçmeyi tercih ederim — erken karmaşıklık, erken optimizasyon kadar risklidir. Kalıcı bir worker isolate kurmak, hata ayıklamayı da karmaşıklaştırır: worker isolate ayrı bir bellek alanında çalıştığı için, oradaki bir hatayı ana isolate'e taşımak da yine mesaj geçişiyle, worker'ın kendi try/catch bloğu içinde yakalayıp sonucu bir hata mesajı olarak SendPort üzerinden geri göndermesiyle olur.
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
Okuyucu Ödülü
Bu yazıyı sonuna kadar okuduğun için, isolate'e geçiş yapmadan önce sırayla kontrol edebileceğin kısa bir karar listesini burada bırakıyorum — bir sonraki ağır iş için doğrudan kopyalayıp kullanabilirsin.
SSS
Dart isolate nedir ve thread'den farkı nedir?
Isolate, kendi belleğine ve tek bir thread üzerinde koşan kendi event loop'una sahip izole bir çalışma birimidir. Klasik işletim sistemi thread'lerinden farkı, bellek paylaşmamasıdır: bir isolate'in global alanları başka hiçbir isolate'ten erişilemez, bu yüzden mutex/lock gibi paylaşımlı-bellek eşzamanlılık araçlarına gerek kalmaz.
Flutter'da compute() ne zaman kullanılır?
compute(), birkaç milisaniyeyi aşan, tek atımlık ve senkron bir hesaplamayı UI thread'ini bloklamadan çalıştırmak istediğinde kullanılır — büyük JSON parse, görsel işleme veya şifreleme gibi. Native platformlarda Isolate.run() ile birebir eşdeğerdir; web'de mevcut event loop'ta çalışır. En fazla bir milisaniye süren işler için SchedulerBinding.scheduleTask tercih edilmelidir.
Isolate'ler arasında nasıl veri paylaşılır?
Veri "paylaşılmaz", mesaj olarak gönderilir; tek kanal SendPort/ReceivePort çiftidir. Çoğu ilkel tür ve gönderilebilir elemanlardan oluşan koleksiyonlar kopyalanarak gönderilir; büyük ikili veri TransferableTypedData ile kopyasız taşınabilir. Native kaynak tutan nesneler (Socket, Pointer, ReceivePort gibi) hiçbir şekilde gönderilemez.
JSON parse işlemi neden UI'yi dondurur?
Çünkü Flutter'da UI thread hem senin uygulama kodunu hem de framework'ün build/layout/paint işlemlerini aynı, tek thread üzerinde çalıştırır. Büyük bir jsonDecode çağrısı bu thread'i senkron olarak meşgul ettiğinde, frame üretimi de aynı sırada beklemek zorunda kalır; sonuç, ~16ms eşiğini aşan ve DevTools'ta kırmızı overlay ile görünen janky frame'lerdir.
Isolate.run() ile compute() arasında hangisini seçmeliyim?
Native platformlarda ikisi birebir aynı mekanizmayı çalıştırır — compute(), native'de await Isolate.run(() => fun(message)) ile eşdeğerdir. Fark platform kapsamındadır: compute() web'de de derlenir ve isolate'lerin desteklenmediği bu ortamda callback'i mevcut event loop üzerinde çalıştırır, Isolate.run() ise saf Dart API'sidir ve web hedefi olmayan bir paket veya komut satırı aracında kullanılır.
Kalıcı worker isolate kurmak ne zaman fazla mühendislik olur?
İş tek atımlıysa veya nadiren tekrarlanıyorsa (ör. kullanıcı ayda birkaç kez büyük bir dosya import ediyorsa), kalıcı bir worker isolate kurmanın getirdiği ek karmaşıklık (yaşam döngüsü yönetimi, hata yayılımı, kanal kurulumu) kazandırdığından daha pahalıya gelir. Flutter'ın tek kesin kuralı burada da geçerlidir: isolate'e — ve dolayısıyla kalıcı worker'a — yalnızca ölçülebilir bir jank varsa geç.
Güncelleme (Eylül 2026)
Bu yazı ilk yayınlandığında (Kasım 2025) Dart 3.9 geçerliydi. O tarihten bugüne bir değişiklik not edilmeye değer: Dart 3.13 (12 Ağustos 2026) dart:isolate kütüphanesine Isolate.runSync, Isolate.create, Isolate.pinToCurrentThread gibi ileri düzey, senkron/event-loop kontrolü sağlayan API'ler ekledi (kaynak: dart-lang/sdk CHANGELOG, 3.13.0 bölümü); bunlar Dart VM C API karşılıkları olan (Dart_CreateIsolateInGroup, Dart_SetCurrentThreadOwnsIsolate) düşük seviyeli, isolate-grubu bağlamlı API'lerdir (kaynak: api.dart.dev Isolate.create / Isolate.pinToCurrentThread) ve bu yazıdaki compute()/Isolate.run() kullanımını etkilemez — API'de bir kırılma yok, compute() ve Isolate.run() hâlâ aynı şekilde çalışıyor.
Sonuç
Isolate kararını her zaman ölçümle ver: önce DevTools'ta profile build ile jank'i doğrula, sonra işin tek atımlık mı yoksa sık tekrarlanan mı olduğuna göre compute()/Isolate.run() ile kalıcı worker isolate arasında seç. Büyük ikili veri taşırken TransferableTypedData'yı, native kaynak tutan nesneleri asla mesaj olarak göndermemeyi unutma.
Bu dört adım — ölç, sınıflandır (tek atımlık mı sık tekrarlanan mı), doğru API'yi seç, gönderilebilirlik kurallarına uy — Dart'ın izole bellek modelini elinden geldiğince az sürtünmeyle kullanmanı sağlar. compute() ve Isolate.run() seni SendPort/ReceivePort'un ayrıntılarından koruduğu için, çoğu projede bu iki API tek başına yeterli olur; kalıcı worker isolate ve TransferableTypedData ise yalnızca ölçümün gerçekten gerektirdiği durumlarda devreye girer.
Konuyu derinleştirmek istersen: state yönetimini isolate'lerle birlikte düşünüyorsan Flutter Riverpod ile State Management rehberine, genel 60fps hedefine Flutter Performans Optimizasyonu yazısına, isolate içeren kodu test etme stratejisi için Flutter Test Rehberi'ne, Dart 3'ün diğer modern özellikleri için Dart 3 Yenilikleri yazısına ve isolate kullanan kodu katmanlara ayırma konusunda Flutter Clean Architecture rehberine göz atabilirsin.
Kaynaklar
- Concurrency in Dart — isolate'lerin bellek izolasyonu, mesaj geçişi ve gönderilemeyen nesne tipleri.
- Isolates — Isolate.run(), SendPort/ReceivePort mekaniği ve worker isolate deseni.
- Isolates | Flutter performance docs — kısa ömürlü isolate maliyeti ve isolate kullanımının tek kesin kuralı.
- Performance view | DevTools — jank eşiği (~16ms), Frame Analysis sekmesi, profile build gerekliliği.
- compute function - foundation library — compute() imzası ve native/web davranış farkı.
- TransferableTypedData class - dart:isolate library — kopyasız, tek kullanımlık veri transferi.
- Dart SDK CHANGELOG (3.13.0) — Isolate.runSync ve ilgili senkron API eklentileri.

