Her Tuş Vuruşunda Çalışan Form Validation Performansı Nasıl Bozar?
Her tuş vuruşunda çalışan form validation, synchronous doğrulama ile re-render zincirini birleştirerek main thread'i gereksiz yere meşgul eder. Debounce, schema bazlı doğrulama ve touched state bu yükü farklı katmanlarda azaltır.
Bir kayıt formu düşünün: kullanıcı e-posta alanına yazmaya başlar. Her harf girişinde validation çalışır, hata mesajları güncellenir, bileşen yeniden render edilir. Kullanıcı "[email protected]" yazana kadar on iki tuşa basar ve her biri bir iş zinciri başlatır. Arayüz tepkisiz görünmez, ama ölçerseniz ana iş parçacığı (main thread) bu sürede gereksiz iş yapar.
Sorun yalnızca hız değildir. Her tuşta çalışan validation, kullanıcı henüz yazmayı bitirmemişken hata gösterir ve formu kullanmayı zorlaştırır. Performans ile deneyim burada iç içe geçer: render maliyetini azaltmak çoğu zaman kullanıcıya daha doğru bir zamanda, daha anlamlı bir geri bildirim göstermek anlamına da gelir.
Synchronous validation ile anlık render güncellemelerini birleştirince maliyet katlanır. Küçük bir forma bile bu zincir, düşük donanımlı cihazlarda tutarsız bir kullanıcı deneyimi yaratır. Debounce, schema bazlı doğrulama ve touched state bu üç sorunun her birine farklı bir yerden müdahale eder; doğru sırayla uygulanınca etkileri birikerek büyür.
Her Tuşun Tetiklediği Zincir
Bir input onChange event'i tetiklendiğinde tipik bir React formunda şu adımlar sırayla çalışır: event handler çağrılır, state güncellenir, bileşen yeniden render edilir, validation fonksiyonu çağrılır, hata state'i güncellenir ve bileşen bir kez daha render edilir. İki render, en az bir kez validation hesabı; hepsi her tuş vuruşunda gerçekleşir.
Validation mantığı basit bir e-posta regex kontrolünden ibaret olduğunda bu zincir hafiftir. Ama formlar büyüdükçe validation da karmaşıklaşır: birden fazla alan birbirine bağlı olabilir, koşullu kurallar devreye girebilir ya da dış veri ile çapraz kontrol yapılabilir. Her tuşta tüm bu kurallar çalışırsa maliyet doğrusal değil, bileşik büyür.
React Profiler ile izlediğinizde bunu somut görürsünüz. Hızlı yazan bir kullanıcı bir saniyede sekiz-on tuşa basabilir. Her tuş iki render demekse, bir saniyede on altı-yirmi render olur. Bunların büyük çoğunluğu kullanıcıya görünmez, ama main thread'i meşgul eder ve bu meşguliyet, kullanıcının başka bir şeye - bir butona tıklamak, bir dropdown açmak - tepki süresini olumsuz etkiler.
Zinciri kırmadan önce neyin zinciri oluşturduğunu anlamak gerekir. Üç bağımsız sorun vardır: doğrulamanın çok sık çağrılması, her çağrının birden fazla state güncellemesine yol açması ve hata bileşenlerinin gereksiz yere render edilmesi. Bu üç sorun farklı teknikleri gerektirir.
Maliyet Neden Görünmez
Tarayıcı geliştirici araçlarında Performance sekmesini açmadan bu maliyeti fark etmek zordur. Kullanıcı şikayet etmez çünkü gecikme küçüktür; ama "küçük" her cihaz için aynı anlama gelmez. Orta seviye bir cihazda birkaç milisaniye süren bir validation döngüsü, düşük uçlu bir cihazda on katına çıkabilir.
Ana iş parçacığı meşgulken kullanıcı etkileşimi de yavaşlar. Tuş vuruşu ile ekranda harfin görünmesi arasındaki süre artar. Bu gecikme 100 milisaniyenin altında kalırsa bilinçli algılanmaz, ama birikerek hissedilir: form "ağır" gibi durur. Kullanıcı bu ağırlığı tanımlayamaz, sadece formu kullanmaktan keyif almadığını fark eder.
Sorun yalnızca validation süresinde değil, çağrılma sıklığındadır. İki saniyede yazan bir kullanıcı için bile validation on beş kez çalıştıysa ve her seferinde state güncellemesi tetiklediyse, render sayısı validation süresinden çok daha fazla katkı sağlamış olabilir. Profiler'da bunu "validation fonksiyonu uzun sürdü" değil, "render çok sık tetiklendi" olarak görürsünüz.
Debounce ile Sıklığı Kesmek
Debounce, bir fonksiyonun son çağrısından belirli bir süre geçmeden tekrar çalışmamasını sağlar. Validation için tipik aralık 200-400 milisaniyedir; bu değer yazma hızına ve form tipine göre ayarlanabilir.
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Kullanım:
const debouncedEmail = useDebounce(email, 300);
useEffect(() => {
if (debouncedEmail) validateEmail(debouncedEmail);
}, [debouncedEmail]);
Bu pattern ile validation her tuşta değil, kullanıcı yazmayı bıraktıktan 300 milisaniye sonra çalışır. Hızlı yazıldığında aradaki tuşlar tamamen atlanır. Render sayısı önemli ölçüde düşer çünkü debounce yalnızca validation'ı değil, ona bağlı her state güncellemesini de erteler.
Debounce'un işe yaramadığı durumlar da vardır. Anlık geri bildirim gerektiren alanlarda - şifre gücü göstergesi, karakter sayacı, eşzamanlı arama önerileri - debounce kullanıcıyı bilgisiz bırakır. Formun amacı burada belirleyicidir: kullanıcı yazdıkça yönlendirilmesi gerekiyorsa, validation'ı ertelemek yerine neyin ertelenip neyin anlık kalacağını ayırt etmek daha doğru bir yaklaşımdır. Şifre gücü göstergesi anlık çalışabilir, "şifreler eşleşiyor mu" kontrolü debounce arkasına alınabilir.
Bir de yanlış yerde debounce sorunu vardır. Debounce'u onChange handler'a değil, validation fonksiyonuna uygulamak yaygın bir hatadır. State her tuşta güncellenmeli, ama validation yalnızca debounce sonrasında çalışmalıdır. Bu ayrımı yapmayan bir uygulama input'un kendisini geciktirir ve kullanıcı yazdığını ekranda geç görür.
Schema Bazlı Doğrulama ve Tek Geçiş
Zod, Yup veya Valibot gibi schema kütüphaneleri validation kurallarını bir kez tanımlayıp her alanda ayrı ayrı yeniden yazmayı önler. Ama performans açısından daha önemli bir faydaları vardır: tüm formu tek bir geçişte doğrularlar.
Elle yazılmış validation'da sık görülen bir anti-pattern şudur: her alan kendi validation fonksiyonunu çağırır, bu fonksiyonlar paylaşılan state'i günceller, her güncelleme yeni bir render tetikler. Üç alanlı bir formda bu yedi-sekiz render anlamına gelebilir; alanlar arası bağımlılık kuralları - şifre teyidi, koşullu zorunluluk - eklenince sayı daha da artar.
const schema = z.object({
email: z.string().email('Geçersiz e-posta'),
password: z.string().min(8, 'En az 8 karakter'),
confirm: z.string()
}).refine(data => data.password === data.confirm, {
path: ['confirm'],
message: 'Şifreler eşleşmiyor'
});
function validateForm(formData) {
const result = schema.safeParse(formData);
if (!result.success) {
return result.error.flatten().fieldErrors;
}
return {};
}
Schema ile tüm form tek seferde parse edilir, sonuç bir kez state'e yazılır. Doğrulama başarısız olsa bile tek bir state güncellemesi ve tek bir render gerçekleşir. Alanlar arası bağımlılık kuralları da aynı geçişte çözülür, ek fonksiyon çağrısı gerekmez.
Schema yaklaşımı her durumda daha hızlı değildir. Şema büyüdükçe parse çağrısı da ağırlaşır. Küçük, bağımsız alanlardan oluşan formlarda elle yazılmış basit bir regex, schema overhead'inden daha az maliyetli olabilir. Karar noktası şudur: form karmaşıklaştıkça ve alanlar arası bağımlılık arttıkça schema kazandırır; form basit ve bağımsız kaldıkça ek bağımlılık olmadan yazılmış doğrudan fonksiyon yeterlidir.
Touched State ile Render Ağacını Küçültmek
Kullanıcı bir alana hiç girmemişken o alan için hata göstermek hem gereksiz bir render hem de kötü bir deneyimdir. Touched state bu ikisini aynı anda çözer.
Touched state, kullanıcının bir alana odaklanıp ayrıldığını izler. Validation her tuşta çalışabilir, ama hata yalnızca touched olan alanlarda gösterilir. Kullanıcı henüz uğramadığı alanlarda hata görmez; ilgili alana girip çıkınca validation sonucu ortaya çıkar. Bu akış hem daha az render hem de daha az kullanıcı sürtüşmesi demektir.
const [touched, setTouched] = useState({});
const [errors, setErrors] = useState({});
function handleBlur(fieldName) {
setTouched(prev => ({ ...prev, [fieldName]: true }));
}
// JSX içinde:
{touched.email && errors.email && (
<span className="error">{errors.email}</span>
)}
Bu pattern ile hata bileşenleri, touched olmayan alanlar için hiç render edilmez. Validation çalışsa bile sonucunu gösterecek bileşen DOM'da yok demektir; hata objesi hesaplanır ama ekrana yazılmaz. Render ağacı küçülür ve bu küçülme sayfanın geri kalanına da yansır: React, değişmeyen bileşenleri reconcile etmek için bile küçük bir zaman harcar.
React Hook Form ve Formik touched state'i otomatik yönetir. Kendi çözümünüzü yazıyorsanız, touched'ı alan başına bir boolean olarak tutmak yeterlidir; ek karmaşıklığa gerek yoktur. Submit butonuna basıldığında tüm alanları bir anda touched yapıp validation sonuçlarını toplu göstermek de yaygın bir kullanım kalıbıdır.
Üç Müdahaleyi Sırayla Uygulamak
Debounce, schema ve touched state bağımsız optimizasyonlar değil, farklı katmanlarda çalışan müdahalelerdir. Birini diğerinin yerine kullanmak sorunu yalnızca kısmen çözer.
Önce touched state'i kurun. En az maliyetli değişikliktir ve render ağacını ilk günden küçük tutar. Formun ne kadar basit ya da karmaşık olduğundan bağımsız olarak her formda işe yarar; geriye dönüp eklemenin riski sıfıra yakındır.
Sonra validation yapısını gözden geçirin. Birden fazla alanın birlikte doğrulandığı, koşullu kuralların olduğu ya da alanlar arası bağımlılıkların bulunduğu her yerde schema render sayısını düşürür. Tek alanlı, bağımsız doğrulama gerektiren formlarda fark daha küçüktür; bu durumda mevcut yapıyı değiştirmeye gerek olmayabilir.
En son debounce ekleyin. Debounce doğruluğu etkileyebilir ve hangi alanlarda kaç milisaniye geciktirileceği test gerektiren bir karardır. Touched state ve schema optimizasyonları olmadan debounce'a geçmek, gereksiz render'ları azaltmak yerine ertelemek demektir. Bu sıra aynı zamanda geri alma maliyetiyle örtüşür: touched state'i değiştirmek risksizdir, debounce süresini değiştirmek bir test çevrimine girer.
Profiler verisi bu sırayı bazen tersine çevirebilir. Validation'ın debounce olmadan açıkça başat maliyet olduğunu görürseniz, önce oraya müdahale etmek mantıklıdır. Kural değil, yol haritasıdır; ölçüm sizi farklı bir sıraya götürürse o sıra doğrudur.
Maliyet, doğrulama mantığının kalitesinden çok sıklığından gelir. Her tuşta çalışan ve her seferinde render zinciri başlatan bir kural, küçük bir formda da birikir. Debounce çağrı aralığını keser; schema tek geçişte karar verir; touched state henüz dokunulmamış alanı zincirin dışında tutar.
Büyük formda üç müdahale Profiler'da görünür. Küçük formda da gereksiz iş yapılmamış olur; alan sayısı arttığında taban çizgisi düşük kalır ve maliyet kendiliğinden birikmez.
Ölçüm yapmadan hangisinin ne kadar katkı sağladığını bilemezsiniz. React DevTools Profiler ya da tarayıcı Performance sekmesi, hangi render'ların ne kadar sürdüğünü gösterir. Optimizasyon yapmadan önce bu veriyi bir kez okumak, doğru müdahaleyi seçmek için çoğunlukla yeterlidir.