Optimistic UI vs Decoupled Akış: Async Backend'ler için UX Desenleri
Async backend'le çalışan tasarımcılar için pragmatik rehber: üç etkileşim şekli, hangisi ne zaman, ve karşı durmanız gereken dört anti-pattern.
Event-based bir backend, işi istek/yanıt döngüsünün dışına taşır: istemci gönderir, sunucu kabul edip kuyruğa alır ve durum değişikliği bir süre sonra gerçekleşir. Spinner, sistemin bu oturum için aktif olarak sonucu ürettiği sinyalini verir; ancak event-based bir akışta client tarafında beklenen bir iş yoktur. UI iki farklı durumu birleştirir (backend işi sıraya aldı vs. backend şu anda sonucu hesaplıyor); oysa bu iki durum kullanıcıya farklı sözler verir.
Backend bir değişikliği kaydettiği an ile frontend’in bunu yansıttığı an arasındaki boşluk mimari olarak kaçınılmazdır. Bu boşluğu spinner ile kapatmak yanlış hamledir: spinner ya durum gelmeden biter (ve kullanıcı “oluşturuldu” yazısının ardından boş bir liste görür) ya da oturumun ötesine sarkar.
Neredeyse her durumu iki yanıt karşılıyor: geri alınabilir aksiyonlar için optimistic UI, geri kalan her şey için decoupled bildirim. Refresh döngüsü dahil dört tanıdık alternatif ise yalnızca çözüm gibi görünüyor.
Üç Seçenek: Ne Zaman Hangisi
Async backend’le çalışırken tasarımcının seçeceği üç etkileşim şekli var.
Optimistic UI, geri alınabilir her aksiyon için varsayılandır. Decoupled bildirim akışları; yavaş, geri alınamaz veya oturumdan uzun yaşayacak her şey için varsayılandır. Bloklayıcı spinner neredeyse her zaman yanlıştır; UI’ı kilitler, kullanıcıyı işi yarıda bırakmak zorunda bırakır ve backend yine de alttan async-güvenli olmak zorundadır. Spinner’ı haklı çıkaracak kadar uzun süren iş, zaten decoupled’a aittir.
Karar tek soruya bağlı: sunucu reddederse kullanıcı bu aksiyonu ekranda tek başına geri alabilir mi?
Ağacın kökü varsayılan; diğer her yaprak, gerekçesini söyleyebilmeniz gereken bir istisna.
Optimistic UI’ı Tasarlamak: Dört Durum
Optimistic UI dört durumlu bir makinedir. Tasarımcının her biri için ayrı bir ekran karşılığı tasarlaması gerekir; atlanan durum genelde rollback olur.
Idle. Varsayılan durum. Buton aktif, form geçerli, imleç hazır. Kullanıcı henüz bir şey yapmamış.
In-flight / Optimistic. Kullanıcının tıkladığı an. Sonucu başarılı olmuş gibi gösterin, ama sessiz bir hedge ile: hafif opaklık, öğenin üstünde küçük bir “gönderiliyor” işareti (tam ekran spinner değil), “geri al” gibi ikincil aksiyonlar devre dışı. Kullanıcı başarıyı hisseder; rollback ihtimaline karşı görsel bir tampon bırakılmıştır.
Confirmed. Sunucudan onay geldi. Hedge kalkar. Geçiş neredeyse görünmez olmalı; kullanıcı zaten başarılı olduğunu düşünüyor, şimdi onu gerçek yapıyorsunuz.
Rolled-back. Sunucu reddetti. Değişikliği geri alın, ama görünür şekilde ve öğenin kendi üzerinde: satırın yanında “gönderilemedi, yeniden dene” yazısı, aynı yerde. Kullanıcının zihinsel modeli: “yaptığım şey burada, aynı yerden tekrar deneyebilirim.”
Üç kural bu dört durumu birbirine bağlar:
- Toast rollback için değildir. Toast kaybolur; kullanıcının neyin başarısız olduğunu ve ne yapabileceğini bilmesi gerekir. Hata, öğenin kendi üzerinde ve yerinde.
- Başarısızlık görsel dili, başarı görsel diliyle aynı olmalı. Aynı satır, aynı etkileşim; sadece durum etiketi ters dönsün.
- Retry, aksiyonun gerçekleştiği yerde. Kullanıcı başka bir sayfaya gidip geri dönmek zorunda kalmasın.
Decoupled Akışı Tasarlamak: Dört Yüzey
Decoupled akışlarda aksiyon kullanıcının gözünün önünden çıkar. UI’ın üç sorumluluğu vardır: kabul edildi mi, nerede görülecek, bittiğinde nasıl haber verilecek?
1. Anlık onay ekranı. Gönderdikten sonra kullanıcıyı bir “aldık” ekranına çıkarın. Kopyada “Başarılı” ya da “Oluşturuldu” değil, “Aldık” ya da “Kabul edildi.” Verebiliyorsanız bir ETA: “Yaklaşık 2 dakika sürecek.” Ve nerede bulacağı: “İşlemlerim sayfasından takip edebilirsin.”
2. Listedeki pending rozeti. Rezervasyon, yükleme, rapor, her neyse, listede hemen görünsün. Ama görünür şekilde pending olarak işaretli: grileşmiş, bir “işleniyor” rozeti, ölçülebiliyorsa satır içi ilerleme çubuğu. Kullanıcı orada olduğunu görür, henüz gerçek olmadığını görür. Refresh döngüsü burada biter.
3. Bildirim yüzeyi. İş bittiğinde kullanıcıya söyleyin. Uygulamadaysa in-app toast, arka planda ise push, değilse e-posta. Her yüzey için hem başarı hem başarısızlık kopyası olmalı: “Rezervasyonun hazır: görüntüle” ve “Rezervasyonun tamamlanamadı: nedeni ve ne yapabilirsin.”
4. Durum sayfası. 30 saniyeden uzun süren her iş için kullanıcının geri dönebileceği özel bir sayfa. Her bildirimden bu sayfaya deep link düşsün. Sayfa tam geçmişi göstermeli: gönderildi → işleniyor → tamamlandı / başarısız.
Dördünün de tabi olduğu tek bir katı sınır var: pending sonsuza dek pending kalamaz. 30 dakikadır “işleniyor” yazan bir satır bir hatadır. Tasarım zamanında bir zaman limiti koyun; o limite ulaşıldığında kullanıcıya açıkça hata gösterin veya eskalasyon yolu verin.
Karşı Durmanız Gereken Dört Anti-Pattern
-
Uzun işlemin üzerinde tam ekran spinner. UI’ı kilitler, kullanıcıyı işi yarıda bırakmak zorunda bırakır, geri dönüş yolu tanımaz. Üç saniyenin üstündeki her işlemi decoupled akışa çekin.
-
Sahte “başarılı” toast’u. “Siparişin oluşturuldu” deyip sonra “ödeme reddedildi” maili göndermek. Kabul ≠ tamamlandı. Kopyanızı gerçeğe göre seçin: “aldık” farklı, “tamamlandı” farklı.
-
Kullanıcı sayfayı bıraktıktan sonra fırlayan hata toast’ı. Toast 3 saniye sonra görünür; kullanıcı başka sayfadadır. Hata, aksiyonun çıktığı öğenin üzerinde, sayfa değişse bile duran yerinde bir durum değişikliği olarak kalmalı.
-
Detail ekranı ile liste ekranı arasında state drift. Modal’da “oluşturuldu” dedi, kullanıcı listeye gitti, orada yok. Klasik read-after-write açığı. Ya optimistic olarak listeye yerleştirin, ya da modal’ı liste güncellenene kadar açık tutun. “Tamam” dedikten sonra kullanıcı “peki nerede?” diye aramak zorunda kalmasın.
Kapanış
Backend async ise, tasarımcının işi aradaki boşluğu tasarlamaktır. Kullanıcı aksiyonu olduğu yerde geri alabildiği sürece varsayılan optimistic UI’dır. Aksiyon geri alınamaz hale geldiğinde, yavaşladığında veya oturumdan uzun yaşayacağı belli olduğunda dürüst yol decoupled’dır; kısa bir loading’i yalnızca gerçekten hızlı sync çağrılar hak eder. Bloklayıcı spinner hiçbir noktada doğru yanıta dönüşmez, çünkü backend’in yapmadığı bir işi anlatır.
Varsayılanı seçin, istisnaları adıyla yazın ve happy path’i göndermeden önce rollback ekranını kurun. Kullanıcılar bir boşluğu refresh döngüsüyle sessizce telafi etmek zorunda kalmamalı.
Dikişin backend tarafını, yani taşıma katmanı seçimini kardeş yazıda ele aldık: Web ve Mobil için Asenkron API Desenleri.
Kaynaklar
- Jakob Nielsen: Response Time Limits - Her UX gecikme bütçesini dayandıran 100ms / 1s / 10s eşikleri.
- Stripe: PaymentIntents lifecycle - Async bir ödeme UX’inin kanonik durum makinesi (processing, requires_action, succeeded).
- Figma: How Figma’s multiplayer technology works - Optimistic UI’ın gerçek zamanlı işbirliği hissini nasıl taşıdığı.
- Reverse engineering Linear’s sync magic - Linear’ın “anında” hissinin arkasındaki optimistic katman.
- Google: How to unsend an email in Gmail - “Gecikme penceresi üzerinden geri alınamaz bir aksiyon için optimistic UI” kanonik deseni.
- Temporal: Handling Signals, Queries, and Updates - Uzun süreli iş ilerlemesini UI’a açmak için kullanılan dil.
İlgili yazılar
Sunucu güdümlü arayüz, sunucu tarafı kompozisyonun mobil karşılığıdır. Zor kısmı JSON render etmek değil, eski sürümlerde ayakta kalan sürümlenmiş bileşen sözleşmesidir.
Mobil binary geri alınamaz ve eski sürümler kalıcıdır; güvenlik ve hız sunucuya kayar: BFF, tüketici güdümlü sözleşmeler ve geriye dönük uyumlu sürümleme.
Mimari ağırlığını runtime'ın init-amortismanına göre seç: single-purpose Lambda'da yalın handler, Lambdalith'te orta, tam OOP/DI yalnızca uzun ömürlü runtime'da.
Bir keşif tezi: event-driven sistemlerde vendor lock-in runtime'da değil bus topolojisinde yaşar; wasmCloud ve NATS bus'ı taşınabilir kılıyor.
SOLID prensiplerinin modern JavaScript'te uygulanışı: TypeScript, React hooks ve fonksiyonel pattern'lerle pratik örnekler, ayrıca ne zaman gereksiz.