React 18'in Concurrent Özellikleri Gerçekte Ne Kazandırır?
React 18'in useTransition, useDeferredValue ve Suspense özellikleri concurrent rendering ile INP'yi nasıl etkiler; hangi durumda gerçek fark yaratır, ne zaman gereksiz ya da ters etki üretir.
React 18 ile gelen concurrent özellikler, tanıtıldıkları günden bu yana pek çok kez yanlış yerde konumlandırıldı. Bir bölümü bu özellikleri sihirli bir hız anahtarı gibi sundu; bir diğer bölüm ise "günlük projelerde gerekmez" diye geçiştirdi. Her iki yaklaşım da eksik kalıyor, çünkü useTransition, useDeferredValue ve güncellenmiş Suspense mekanizmasının kazandırdığı şey belirli koşullara sıkı sıkıya bağlıdır.
Concurrent rendering, React'in tarayıcıya render işini tek seferde teslim etmek yerine parçalara bölerek sunmasına olanak tanır. Bu sayede uzun süren bir güncelleme, kullanıcı etkileşiminin önüne geçemez. Ama bu yeteneğin devreye girebilmesi için hangi güncellemelerin ertelenebilir, hangilerinin acil olduğunu açıkça bildirmeniz gerekir; React bunu siz belirtmeden otomatik olarak anlayamaz.
Bu üç özelliğin gerçek kullanım sınırlarını, INP (Interaction to Next Paint) üzerindeki etkisini ve ne zaman hiçbir işe yaramadığını, hatta ters etki ürettiğini aşağıda ele alıyoruz.
Bloklama Render'ın INP Üzerindeki Etkisi
React 17 ve öncesinde tüm state güncellemeleri senkron çalışıyordu. Bir butona tıkladığınızda React, güncellemeyle başlayıp bileşen ağacını tamamen boyayıncaya kadar main thread'i elinde tutuyordu. Bu süre zarfında klavye girişleri, fare hareketleri ve diğer etkileşimler kuyrukta bekliyordu. Render süresi 50 milisaniyenin altında kaldığında bu durum sorun yaratmıyordu; kullanıcı gecikmeyi fark etmiyordu.
Sorun, aynı etkileşim sonucu ağır bir bileşen ağacının boyandığı durumlarda ortaya çıkıyordu. Büyük bir liste filtreleme işlemi, çok sayıda bağımlı bileşeni tetikleyen bir tab değişikliği ya da karmaşık bir form güncellemesi 200-400 milisaniyeyi bulduğunda INP değerleri artıyordu. Core Web Vitals ölçümlerinde INP, kullanıcının bir sayfayla etkileşime girdiği andan tarayıcının görsel geri bildirim verdiği ana kadar geçen süreyi ölçer; bloklayan render bu aralığı doğrudan uzatır.
Concurrent rendering, render işini "fiber" adı verilen küçük birimlere böler ve bu birimler arasında tarayıcının yüksek öncelikli görevlere zaman ayırmasına izin verir. Ama bu mekanizmanın çalışması için React'in hangi güncellemenin ertelenebileceğini bilmesi şarttır. İşte bu noktada useTransition devreye girer.
useTransition: Acil Olmayan Güncellemeleri İşaretlemek
Yavaş çalışır. Bu, useTransition'ın en kolay hatırlanan tanımı, ama tam olarak doğru değil. useTransition, bir güncellemeyi yavaşlatmaz; tarayıcıya "bu güncelleme bekleyebilir, kullanıcı etkileşimini önceliklendirirsen razıyım" der. Render işi aynı thread'de yapılmaya devam eder; fark, bu işin kesintisiz olması yerine araya giren etkileşimlere yer açacak şekilde planlanmasıdır.
Kullanımı şöyledir:
const [isPending, startTransition] = useTransition();
function handleTabChange(tab) {
startTransition(() => {
setActiveTab(tab);
});
}
startTransition içine sarılan state güncellemesi, React tarafından düşük öncelikli olarak işaretlenir. Kullanıcı bu süre içinde başka bir etkileşim yaparsa React, devam eden render'ı kesip etkileşimi işler ve ardından transition'a döner. isPending bayrağı güncelleme henüz tamamlanmamışken true döner; bu değeri yükleniyor göstergesi için kullanabilirsiniz.
Bunun anlamlı olduğu durumlar oldukça spesifiktir. Bir arama kutusu düşünün: kullanıcı yazdıkça hem input değeri hem de sonuç listesi güncelleniyor. Input güncellemesi acildir, çünkü kullanıcı yazdığının ekranda anında göründüğünü bekler. Sonuç listesinin güncellenmesi ertelenebilir; kullanıcı yazmayı sürdürüyorsa zaten bir önceki sonuç listesi geçersiz kalacaktır. Input güncellemesini doğrudan, sonuç listesi güncellemesini startTransition içinde yaparsanız input yanıt vermeye devam eder, sonuç listesi ise kullanıcı duraklayana kadar beklemeye alınır.
Aynı mantık büyük tab geçişlerine, ağır panel açma-kapama işlemlerine ve filtreleme sonucu uzun liste render'larına uygulanabilir. Çok bileşenli uygulamalarda render maliyetinin nereden kaynaklandığını profil verisiyle görmek, startTransition'ın hangi noktada uygulanması gerektiğini çok daha net ortaya koyar.
Ne zaman işe yaramaz? Render süresi zaten 50 milisaniyenin altında kalıyorsa. Concurrent rendering'in planlamaya eklediği ek yük, bu sürede kazanılacak faydayla yarışır. Küçük bileşen ağaçlarında, sınırlı state değişikliklerinde useTransition eklemek kodu karmaşıklaştırır ama ölçülebilir bir iyileştirme getirmez.
useDeferredValue: Prop Düzeyinde Erteleme
useTransition, state güncellemesinin kaynağına müdahale eder; useDeferredValue ise bir değerin tüketilme anını erteler. İkisi aynı sorunu farklı açılardan ele alır ve hangisini seçeceğiniz çoğunlukla kontrolün nerede olduğuna bağlıdır.
Eğer state'i doğrudan siz yönetiyorsanız useTransition daha açık bir ifadedir. Ancak değer bir üst bileşenden prop olarak geliyorsa ve siz yalnızca tüketiciyseniz, useDeferredValue devreye girer:
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
return <ResultList query={deferredQuery} />;
}
Burada query her değiştiğinde bileşen hemen yeniden render olur, ama ResultList'e geçirilen deferredQuery bir "geride kalan" sürümü taşır. Tarayıcı yoğunken bu ertelenmiş değer güncellenmez; yük hafifler. Kullanıcı yazmayı duraksattığında değer güncellenir ve ResultList yeni sorguyla birlikte render edilir.
useDeferredValue'nun ikinci bir avantajı daha vardır: React.memo ile sarılmış bir bileşene ertelenmiş değer geçirildiğinde, değer referans olarak aynı kaldığı süre boyunca memo devreye girer ve yeniden render engellenir. React.memo'nun referans kararlılığına nasıl bağımlı olduğunu anlamak, bu iki mekanizmayı neden birlikte kullandığınızı çok daha iyi açıklar.
Throttle veya debounce ile karıştırmamak gerekir. Debounce sabit bir gecikme ekler; tarayıcı boşta olsa bile bekleme süresi geçmeden güncelleme yapılmaz. useDeferredValue ise tarayıcının mevcut yüküne göre uyarlanır; sistem boşta olduğunda gecikme sıfıra yaklaşır. Sabit gecikme eklemek yerine sisteme bırakmak, çoğu durumda daha doğru bir sonuç üretir.
Suspense ile Veri Yükleme Akışını Yönetmek
React 18 öncesinde Suspense yalnızca kod bölme için kullanılıyordu; veri yükleme senaryolarında resmi destek yoktu. React 18 ile birlikte Suspense, veri kaynaklarını da kapsayacak şekilde genişledi. Bu değişiklik, veri yükleme durumlarını bileşen ağacının neresinde yakalayacağınızı yeniden düşünmenizi sağlıyor.
Klasik yaklaşımda her bileşen kendi yükleniyor durumunu yönetirdi; bu durum hem tekrarlayan kod hem de birbirinden bağımsız titreyen yükleniyor göstergeleri anlamına geliyordu. Suspense ile bir üst sınır belirleyebilir, birden fazla bileşenin yüklenmesini tek bir yükleniyor durumu altında toplayabilirsiniz. Kullanıcı deneyimi açısından bu fark büyüktür; tek bir iskelet ekran görmekvs. ardı ardına beliren farklı yükleniyor göstergelerini takip etmek birbirinden farklı hissettiriyordur.
Concurrent rendering ile birleşince Suspense'in bir özelliği daha anlamlı hale gelir: yeni bir route veya tab yüklenirken React, önceki içeriği ekranda tutmaya devam edebilir. Başka bir deyişle, yeni sayfa hazır olana kadar kullanıcı boş bir ekranla değil hâlâ etkileşim kurulabilir mevcut içerikle karşı karşıya kalır. Bu davranış startTransition ile birlikte kullanıldığında, geçiş tamamlanıncaya kadar eski içeriğin görünmesi sağlanabilir.
Ancak bir sınır vardır: Suspense için veri kaynaklarınızın bu mekanizmayı desteklemesi gerekir. Next.js App Router, Relay ve bazı React Query sürümleri bu desteği sunar. Kendinizin yazdığı fetch mantığı, özellikle useEffect içindeki klasik veri çekme kodları, Suspense ile uyumlu çalışmaz. useEffect içinde veri çekme hâlâ yaygın olmakla birlikte, Suspense entegrasyonu söz konusu olduğunda ayrı bir uyarlama süreci gerektiriyor.
Concurrent Özelliklerin Gerekmediği ve Ters Etki Ürettiği Durumlar
Her araç kendi sorununu çözer. Render süresi kısa uygulamalarda, yani bileşen ağacı sığ, state geçişleri hızlı olan projelerde useTransition ve useDeferredValue eklemek ölçülebilir bir iyileştirme getirmez. Aksine, kod okunabilirliğini düşürür ve bakımı zorlaştırır.
Ters etki söz konusu olduğunda ise iki senaryo öne çıkar. Birincisi: bir form alanını transition ile sararsanız kullanıcı kendi yazdığını gecikmeli görür. Input güncellemesi hiçbir zaman transition içine alınmamalı; yalnızca o input'a bağımlı ikincil güncellemeler sarılabilir. İkincisi: çok sık değişen bir state'i useDeferredValue ile ertelerseniz, her render döngüsünde iki farklı değer sürümü aynı anda var olmaya başlar; bu durum bileşen çıktısında tutarsızlık riski taşır.
useMemo ve useCallback'in gereksiz kullanımı nasıl performans yerine ek maliyet üretiyorsa, concurrent özelliklerinin yerli yersiz kullanımı da aynı tuzağa düşer. Her optimizasyon aracının bir maliyeti vardır; bu maliyetin altında kalan sorunlara uygulamak net kazanç yerine net kayıp üretir.
Context API ile birlikte kullanıldığında da dikkatli olmak gerekir. startTransition içinde bir Context değerini güncellediğinizde, o Context'i tüketen tüm bileşenler transition kapsamında yeniden render olur. Context güncelleme sıklığı yüksekse bu beklenen davranışı karmaşık hale getirebilir. Context API'nin re-render yayılması zaten başlı başına karmaşık bir konudur; transition eklenince izlenmesi daha da güçleşir.
Etkiyi Ölçmek: Hangi Metrik, Hangi Araç
Concurrent özelliklerini uygulamadan önce ve sonra ölçmek, kazanım olup olmadığını belirlemenin tek güvenilir yoludur. Profiler olmadan yapılan değerlendirmeler sezgiye dayanır; sezgi ise render performansında çoğu durumda yanıltıcıdır.
Chrome DevTools Performance paneli, React uygulamalarında render süresini milisaniye cinsinden gösterir. React DevTools Profiler ise hangi bileşenin kaç milisaniye harcadığını, hangi render'ın hangi state değişikliğinden kaynaklandığını açık eder. startTransition uyguladıktan sonra Profiler'da transition olarak işaretlenmiş render bloklarını görmek, mekanizmanın gerçekten devreye girdiğini doğrulamanın en doğrudan yoludur.
INP ölçümü için ise gerçek kullanıcı verisi (RUM) gerekir; laboratuvar ölçümleri INP'yi tam yansıtmaz. Google Search Console'daki Core Web Vitals raporu ve web-vitals kütüphanesiyle yerleştirilen anlık ölçümler, concurrent özelliklerin gerçek kullanıcılar için fark yaratıp yaratmadığını gösterir. Profiler'da olumlu görünen ama RUM verilerinde yansımayan değişiklikler, iyileştirmenin kullanıcının gerçek cihaz ve bağlantı koşullarında etkisiz kaldığına işaret eder. Yayına çıkmadan önce yapılacak performans kontrolleri arasında bu tür doğrulama adımları kritik yer tutar.
Concurrent özelliklerinin sunduğu asıl değer, uygulamanın daha hızlı render etmesi değildir. Değer, ağır render'ların kullanıcı etkileşimini kesmemesindedir. Bu ayrım önemlidir: eğer render süresi zaten düşükse concurrent özellikler fark yaratmaz; render süresini düşürmek için ise başka araçlara ihtiyaç vardır. Kod bölme ve sayfa sadeleştirme gibi yapısal kararlar, concurrent özelliklerle tamamlayıcı bir ilişki kurar; birini diğerinin yerine geçirmek yanlış çerçevelemedir.
Sonuçta bu üç özellik, React'in uzun süredir eksik olduğu bir yeteneği kapatıyor: etkileşim önceliğini kod düzeyinde ifade edebilmek. Ama her yeteneğin bir kullanım koşulu vardır. Render sürenizi ölçün, bottleneck'in gerçekten uzun bileşen render'larından kaynaklandığını doğrulayın, ardından hangi güncellemelerin ertelenebilir olduğuna karar verin. Kör uygulama ne kazandırır ne de öğretir.