FLUTTER PERFORMANS VE REBUILD

Flutter logosunun parlak mavi 3D rendering hali ile widget rebuild kavramının marka kimliğindeki temsili

Listede aşağı kaydırırken telefonun ısındığını fark edersiniz. Debug modda Performance Overlay açıktır, üstteki UI grafiği sürekli kırmızı çıkar. Hiçbir şey eklemediniz — sadece bir setState en üstteki widget'a girdi ve şimdi her saniye ekranın yarısı yeniden çiziliyor. Flutter'da en sık karşılaşılan performans sorunu bug değildir; gereksiz rebuild'tir ve çoğu zaman birkaç saatlik dikkatle düzeltilir.

Bu yazı 60 FPS hedefinden sapan Flutter uygulamalarında rebuild kaynaklı yavaşlamayı nasıl bulup nasıl gidereceğinizi anlatır. Mobil tarafta ürün geliştirenler, performans regresyonu raporları gelen ekipler ve Flutter'ın iç mantığını netleştirmek isteyen herkes için pratik bir başlangıçtır. Flutter'ın resmi performans dokümantasyonu arka plandaki render boru hattını çok detaylı anlatır; biz buradaki başlıkları günlük geliştirici diline indireceğiz.

Flutter Neden Sürekli Yeniden Çiziyor?

Flutter'ın render mantığı üç ağaç üzerinde çalışır: Widget, Element ve RenderObject. Widget ağacı ucuzdur, sürekli yeniden oluşturulur. Element ağacı state tutar. RenderObject ağacı asıl pixel'i çizer. Performans maliyeti büyük ölçüde RenderObject seviyesindedir, ama tetik widget seviyesinden gelir.

Bir StatefulWidget'ın setState'i çağrıldığında o State'in build metodu yeniden çalışır. Geri dönen widget alt ağacı eski ağaçla karşılaştırılır. Burada Flutter akıllıdır — değişmemiş alt ağaçları reuse etmeye çalışır. Sorun şudur: yeni oluşan widget instance'ları eskisiyle aynı tipte ve aynı parametrede bile olsa, eğer const değilse Flutter "belki değişti" diye kabul edip Element ağacında karşılaştırma maliyetine girer. Yüzlerce widget'lık bir ağaçta bu maliyet birikir.

Gerçek senaryo: bir Scaffold'ın en üstündeki AppBar dakikada bir saatin tickleriyle güncelleniyor. Onun altındaki body'de 200 elemanlı bir ListView var. AppBar'ı tutan State setState çağırınca, body de teorik olarak yeniden build edilir. Eğer body içindeki ListView const değilse Flutter her tick'te tüm liste yapısını tekrar gezecek demektir. Pixel'e bir şey çizilmese bile CPU ısınır.

Performans Sorununu Nereden Anlarsınız?

Tahminle optimizasyon olmaz. Ölçüm önce gelir. Flutter üç ana ölçüm aracı sunar:

  • Performance Overlay: Debug build'de iki şerit gösterir — UI thread ve raster thread. 16ms'nin üzerine çıkan kareler kırmızıya döner.
  • DevTools Performance sekmesi: Frame frame zaman çizgisi, hangi widget'ın ne kadar build süresi aldığını gösterir.
  • Widget Inspector — Track Widget Builds: Hangi widget'ın kaç kez rebuild edildiğini sayar. Asıl altın madeni budur.

Track Widget Builds'i aç, bir kullanıcı senaryosunu (scroll, tap, animasyon) tekrarla, sonra sayaca bak. Her saniyede 60 kez rebuild olan bir static AppBar gördüğünde sorun lokalize olmuştur. Genel "her şey yavaş" hissi yerine "bu widget gereksiz rebuild ediyor" tespiti var.

Flutter widget ağacında setState sonrası sadece scoped alt ağacın yeniden çizildiği rebuild kapsamı diyagramı

Const Constructor — En Ucuz Optimizasyon

Flutter'da const en az tanınan ama en yüksek getirili optimizasyondur. Bir widget'ın constructor'ı const olarak işaretlenip parametre olarak compile-time sabitleri alıyorsa, Flutter o widget'ı tek bir instance olarak tutar. Yeniden oluşturma yoktur, karşılaştırma maliyeti yoktur, Element ağacında yerinde durur.

Pratik örnek — kötü:

  • Padding(padding: EdgeInsets.all(8), child: Text('Yükleniyor'))

İyi:

  • const Padding(padding: EdgeInsets.all(8), child: Text('Yükleniyor'))

Tek kelimelik fark, üstteki state her tick ettiğinde bu Padding'in maliyetini sıfıra indirir. Yeni başlayanlar genellikle const'u sadece linter uyarısıyla eklemeye başlar. Daha iyi yaklaşım: flutter_lints paketindeki prefer_const_constructors kuralını analysis_options.yaml'da hata seviyesine çıkarmaktır. CI bunu yakalar, hiç compile-time sabit olabilecek bir widget const'suz birleşemez.

State'i Doğru Scope'a Yerleştirme

İkinci büyük rebuild kaynağı state'in çok yüksekte tutulmasıdır. Tipik yanlış: kullanıcının karanlık tema açık mı kapalı mı bilgisi MaterialApp seviyesindeki bir StatefulWidget'ta tutuluyor. Tema toggle'lanınca tüm uygulama yeniden build edilir. Bu doğru. Ama aynı seviyeli widget bir saniye sayacı da tutuyorsa, her saniye tüm uygulama yeniden build edilir. Bu yanlış.

Çözüm: state'i mümkün olan en alttaki widget'a indirin. Saniye sayacı için en altta küçük bir StatefulWidget açın, sadece o widget tick alsın. Üst kısımlar etkilenmez.

Provider, Riverpod, BLoC gibi state management kütüphaneleri bu sorunu yapısal olarak çözer. Provider'ın Selector widget'ı, model'in yalnızca seçilen field'ı değişince rebuild eder:

  • Selector<UserModel, String>(selector: (_, m) => m.name, builder: ...)

UserModel'in başka bir field'ı (örneğin avatar URL'i) değişse bile name değişmediği sürece builder çalışmaz. Doğru kullanıldığında 200 widget'lık bir ekranda sadece etkilenen 5 widget rebuild olur.

State yönetiminin bu yönüne pratik olarak hakim olmak için Flutter eğitimi kapsamında Provider ve Riverpod örnekleriyle scope kararları işlenir.

Liste Optimizasyonu

Uzun listeler Flutter performansının klasik tuzağıdır. Column içinde map ile 500 widget üretmek hatalıdır — tümü aynı anda inşa edilir, scroll alanı dışında olanlar bile bellekte tutulur. Doğru yöntem:

  • ListView.builder: Lazy oluşturur, sadece ekrandaki + buffer kadar item inşa eder
  • itemExtent veya prototypeItem: Sabit yükseklikli item'lerde scroll matematiği ucuzlar
  • const item builder: Her item widget'ı mümkün olduğunca const
  • Key kullanımı: Item sırası değişiyorsa ValueKey ile kimliklendirin

Bir item içinde maliyetli bir kısım varsa (örneğin animasyonlu profil avatarı), o kısmı ayrı bir StatefulWidget'a çıkarın. Item rebuild olduğunda animasyon state'i kaybolmaz, sadece statik kısım yeniden çizilir.

Maliyetli Widget'ı İzole Etme

Bazen state'i daha aşağı indirmek mümkün olmaz çünkü değer birden fazla yerde gerekir. Bu durumda ValueListenableBuilder doğru araçtır. Bir ValueNotifier<int> tanımlarsınız, sadece bu değeri dinleyen bölüm ValueListenableBuilder içine alınır. Notifier değiştiğinde yalnızca builder içindeki ağaç yeniden çalışır, etrafındaki widget'lar sabit kalır.

Mantığı şuna benzetin: AppBar'ın sağına bir sepet sayacı koyacaksınız. Sayaç her ürün eklendiğinde değişir. AppBar'ın tamamını rebuild etmek yerine sayacı ValueListenableBuilder ile sarın. Sepete 50 kez ekleme yapıldığında AppBar'ın geri kalanı (logo, ikon, arama kutusu) bir kez bile yeniden çizilmez.

Animasyonlar İçin Özel Pattern

AnimationController her tick'te değer üretir — saniyede 60 kez. Eğer setState içinde kullanırsanız her tick tüm widget'ı rebuild edersiniz. Doğrusu AnimatedBuilder ile sadece animasyon değişen kısmı sarmaktır. Builder'ın child parametresi de işin sırrıdır: child statik kısmı tutar, builder her tick'te o statik child'ı reuse eder, sadece transform/opacity gibi dinamik kısmı yeniden hesaplar.

Aynı mantık AnimatedSwitcher, TweenAnimationBuilder, Hero gibi widget'lar için de geçerlidir. Hepsi rebuild'i izole eder, yalnız gerçekten değişen pixel'i yeniden çizdirir.

Const olmayan ve const işaretli widget arasında yeniden oluşturma maliyetini karşılaştıran Flutter optimizasyon diyagramı

Ölçüm Olmadan Optimizasyon Yok

Bu yazıdaki tekniklerin hepsi makul kullanıldığında işe yarar; körü körüne uygulandığında ise tersine etki eder. Her widget'ı const yapmak için ağacı parçalamaya çalışmak okunabilirliği bozar ve gerçekte ölçülmeyen optimizasyon hayali bir kazançtır. Aynı şekilde her şeyi Selector veya ValueListenableBuilder'a sarmak abartıdır — küçük uygulamalarda gereksiz karmaşıklık yaratır.

Doğru sıra şudur: önce DevTools'da darboğazı bul, sonra o widget'a uygula, sonra ölç ve farkı doğrula. Tahmin değil, ölçüm. Flutter performansının altın kuralı budur. 60 FPS hedefine ulaşmak çoğunlukla yeni özellik eklemek kadar zevkli iştir — sadece doğru yerden başlamak gerekir.