Yazılarımız

Veri Akademi

FLUTTER PERFORMANS: WİDGET REBUİLD DAVRANIŞI VE OPTİMİZASYON TEKNİKLERİ

Flutter’da “performans” çoğu zaman ham cihaz gücünden değil, yeniden inşa (rebuild) ve çizim (render) kararlarının ne kadar isabetli verildiğinden gelir. Akıcı görünen bir ekran ile takılan bir ekran arasındaki fark, çoğu kez birkaç küçük mimari tercihin toplam etkisidir.

Bu yazıda, widget rebuild davranışını pratik bir bakışla ele alacağız: Neler rebuild tetikler, rebuild her zaman kötü müdür, hangi noktalarda maliyet büyür ve hangi optimizasyon teknikleri gerçek hayatta en çok faydayı sağlar.

Hedefimiz “micro-optimizasyon” değil; önce doğru ölçmek, sonra doğru yeri iyileştirmek. Böylece kod okunabilirliğini bozmadan, jank riskini düşürüp daha stabil frame süreleri elde edebilirsiniz.


Primary odak: Flutter performans için rebuild’i anlamak

Bu makalenin primary keyword odağı Flutter performans. Performansı etkileyen birçok katman olsa da, UI tarafında en sık karşılaşılan sorunlardan biri, beklenmeyen ölçekte widget ağacının yeniden build edilmesidir. Rebuild, Flutter’ın doğal çalışma biçimidir; asıl mesele rebuild’in kapsamını ve sıklığını yönetmektir.

Şunu netleştirelim: Rebuild “otomatik olarak kötü” değildir. Flutter, hızlı rebuild yapacak şekilde tasarlanmıştır. Sorun, rebuild’in pahalı işler (ağ çağrısı, ağır hesaplama, büyük liste diff’i, gereksiz layout/paint) ile birleşmesi veya çok geniş subtree’leri etkilemesidir.

Widget ağacında state değişiminin hangi alt dalları etkilediğini sezdiren akış diyagramı ve örnek ekran düzeni

Widget, Element ve RenderObject ayrımı neden kritik?

Flutter’da “widget” çoğu zaman yanlış anlaşılır. Widget, çoğunlukla immutable bir konfigürasyon nesnesidir. Asıl yaşam döngüsü ve bağlam, Element tarafında akar. RenderObject ise çizim ve layout maliyetini belirleyen katmandır. Bir widget rebuild olduğunda, bu her zaman yeniden paint anlamına gelmez; ancak yapı doğru kurgulanmadıysa rebuild zinciri render katmanına daha fazla iş taşır.

Bu ayrımı bilmek, “her build bir felaket” algısını kırar ve optimizasyonu doğru katmana yönlendirir: bazen hedef, rebuild’i azaltmak değil, rebuild sırasında yapılan işleri hafifletmektir.

Rebuild tetikleyicileri: Sadece setState değil

En yaygın tetikleyici setState olsa da rebuild şu durumlarda da yayılır: ebeveyn widget’ın rebuild olması, inherited yapılar (örn. tema, yerelleştirme, state sağlayıcılar) üzerinden değişiklik gelmesi, MediaQuery/Orientation değişimleri, animasyon tick’leri ve listelerde item güncellemeleri. Burada kritik nokta, tetikleyicinin doğal olup olmadığı değil; güncellemenin hangi subtree’yi etkilediğidir.


Build metodu: Hızlı olmalı, yan etkisiz kalmalı

Performans sorunlarının büyük kısmı build metoduna yanlış işler yüklenince başlar. Build içinde ağ isteği, dosya okuma, ağır JSON parse veya büyük veri manipülasyonu yapmak, frame süresini uzatır. Build; mümkün olduğunca pure olmalı, yani sadece mevcut state’ten UI üretmelidir.

Build’in sık çağrılacağını kabullenmek gerekir. Bu yüzden “build içinde hesaplarım” yaklaşımı, ölçeksiz büyür. Hesaplamayı önceden cache’lemek, memoization uygulamak veya hesaplamayı isolate’a taşımak gibi stratejiler, rebuild maliyetini stabilize eder.

Yan etki kokusu: Build içinde yapılan pahalı işler

Aşağıdakiler build içinde görünüyorsa genelde performans borcu birikir: uzun süren string formatlamaları, büyük liste filtreleme/sıralama, image decode tetikleyen senaryolar, her rebuild’de yeni controller yaratma, her rebuild’de yeni stream/observable bağlama. Bunlar “tek seferde çalışıyor gibi” görünse de, rebuild sayısı arttıkça maliyet katlanır.

Const kullanımı: Küçük dokunuş, büyük etki

const widget kullanımı, Flutter’ın aynı konfigürasyonu tekrar tekrar üretmesini engelleyerek GC baskısını azaltır ve bazı durumlarda subtree’nin daha stabil kalmasını sağlar. const, her şeyi çözmez; ancak özellikle statik UI parçalarında ciddi bir temizlik sağlar. Ayrıca kodu daha deterministik hale getirir.


Gereksiz rebuild’lerin tipik nedenleri

Rebuild’i azaltmak için önce nerede gereksiz büyüdüğünü görmek gerekir. Bu bölümde, sahada en sık rastlanan kaynakları toparlayalım. Buradaki secondary keyword kümeleri; widget rebuild, setState kapsamı, state management performansı, profiling ve frame rendering gibi kavramlar etrafında kurgulanmıştır.

setState kapsamının fazla geniş tutulması

Bir ekranın tamamını tek bir StatefulWidget içine koyup her küçük değişimde tüm ekranı rebuild etmek, özellikle listeler ve karmaşık layout’larda sorun çıkarır. setState ile işaretlenen şey, o State’in altındaki tüm build sürecidir. Bu yüzden state’i küçük bileşenlere bölmek ve güncellemenin etkilediği alanı daraltmak daha sağlıklıdır.

<!-- Örnek 1: Gereksiz geniş rebuild -->
<!-- Dart kodu: -->
class CounterPage extends StatefulWidget {
  const CounterPage({super.key});
  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    // Bu build her count artışında komple yeniden çalışır
    return Scaffold(
      appBar: AppBar(title: const Text('Sayaç')),
      body: Column(
        children: [
          Text('Değer: $count'),
          // Büyük bir liste veya pahalı widget ağacı burada olsaydı,
          // her artırmada aynı subtree de rebuild olacaktı.
          Expanded(
            child: ListView.builder(
              itemCount: 200,
              itemBuilder: (_, i) => ListTile(title: Text('Satır $i')),
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() => count++),
        child: const Icon(Icons.add),
      ),
    );
  }
}

Bu örnekte sorun “liste var” olması değil; listenin, sayaç değişimiyle ilgisiz olduğu halde her artışta yeniden build akışına girmesidir. İyileştirme için sayaç bölümünü küçük bir widget’a ayırmak veya dinamik parçayı ayrı bir builder ile izole etmek daha uygundur.

Her build’de yeni nesneler üretmek

Birçok jank vakası, build içinde sürekli yeni liste/map üretiminden çıkar. Örneğin her build’de items.map(...).toList() yapmak, küçük veride fark edilmeyebilir; ancak veri büyüyünce GC ve CPU maliyeti belirginleşir. Aynı şekilde her build’de yeni TextStyle, yeni EdgeInsets gibi nesneler üretilmesi de birikir (const ile azaltılabilir).

Keys yanlış veya rastgele kullanımı

Key’ler doğru kullanıldığında element eşleşmesini stabilize eder; yanlış kullanıldığında Flutter’ın “bu başka bir şey” diyerek daha fazla işi yeniden kurmasına yol açabilir. Özellikle UniqueKey() gibi her build’de değişen key üretmek, element ağacını sürekli bozabilir. Key stratejisi; veri kimliği ile uyumlu ve kalıcı olmalıdır.

Liste öğelerinde stabil kimlikler ve key yaklaşımıyla kaydırma sırasında tutarlı element eşleşmesini anlatan örnek düzen

Optimizasyon teknikleri: Kapsam daraltma ve yeniden kullanım

Rebuild optimizasyonunda en büyük kazanım, güncellemeyi ilgisiz alanlardan ayırmaktır. Bunun için birkaç etkili yaklaşım var: widget parçalama, builder’larla izolasyon, state yönetiminde seçici dinleme, const kullanımı ve doğru lifecycle yönetimi.

Widget parçalama: Okunabilirlik + performans

Büyük build metotları, hem okunabilirliği düşürür hem de “ne rebuild oluyor” sorusunu belirsizleştirir. UI’ı mantıksal bloklara ayırmak, performans açısından da avantaj sağlar: küçük widget’lar daha hedefli güncellenir, test edilmesi kolaylaşır ve gereksiz bağımlılıklar azalır.

Builder ile izolasyon: Sadece gereken bölümü güncelle

Dinamik bir değeri sadece küçük bir bölgede kullanıyorsanız, o bölgeyi ayrı bir builder yapısı içinde tutmak rebuild kapsamını küçültür. Örneğin ValueListenableBuilder veya seçici dinleme yaklaşımı, state değişince sadece ilgili subtree’nin yenilenmesini sağlar.

<!-- Örnek 2: Seçici rebuild ile izolasyon -->
<!-- Dart kodu: -->
class CounterHeader extends StatelessWidget {
  const CounterHeader({required this.count, super.key});
  final int count;

  @override
  Widget build(BuildContext context) {
    return Text(
      'Değer: $count',
      style: Theme.of(context).textTheme.headlineSmall,
    );
  }
}

class BetterCounterPage extends StatefulWidget {
  const BetterCounterPage({super.key});
  @override
  State<BetterCounterPage> createState() => _BetterCounterPageState();
}

class _BetterCounterPageState extends State<BetterCounterPage> {
  final ValueNotifier<int> count = ValueNotifier<int>(0);

  @override
  void dispose() {
    count.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sayaç')),
      body: Column(
        children: [
          ValueListenableBuilder<int>(
            valueListenable: count,
            builder: (_, value, __) => CounterHeader(count: value),
          ),
          const Expanded(
            child: _HeavyList(), // const ile stabilize
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => count.value++,
        child: const Icon(Icons.add),
      ),
    );
  }
}

class _HeavyList extends StatelessWidget {
  const _HeavyList();

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 200,
      itemBuilder: (_, i) => ListTile(title: Text('Satır $i')),
    );
  }
}

Bu yaklaşımla sayaç değiştiğinde yalnızca başlık kısmı rebuild olur; liste ise stabil kalır. Bu, özellikle karmaşık item widget’ları veya zengin görseller içeren listelerde fark edilir iyileştirme sağlar.

State management performansı: “select” yaklaşımı

Provider, Riverpod, Bloc gibi çözümlerde, tüm model değiştiğinde tüm dinleyicilerin rebuild olması tipik bir tuzaktır. Seçici dinleme (ör. sadece gereken alanı izlemek) ile güncelleme dalgasını küçültmek mümkün olur. Buradaki amaç, state yönetim aracını değiştirmek değil; aracı doğru kullanmaktır.

İç link ile derinleşme

Bu konuları uygulamalı örneklerle ve gerçek projelerdeki anti-pattern’lerle öğrenmek isterseniz Flutter eğitimi sayfasındaki modüller, performans ve mimari kararları birlikte ele alır.


Profiling: Ölçmeden optimize etme

Performans iyileştirmesi bir hipotez işidir: “Burası yavaş olabilir” dersiniz, ölçersiniz, doğrularsınız ve hedefli düzeltirsiniz. Flutter DevTools bu süreçte en değerli araçtır. Burada amaç, sadece FPS’e bakmak değil; frame zamanlarını ve hangi aşamada takıldığını görmektir.

Timeline okumak: UI thread mi, raster mı?

Jank, UI thread’in build/layout tarafında da çıkabilir, raster tarafında da. Eğer UI tarafında süre uzuyorsa rebuild ve hesaplamalar şüphelidir; raster tarafında uzuyorsa çizim/efekt/overdraw gibi konular öne çıkar. Rebuild odaklı bir makalede bile bu ayrım önemlidir; çünkü bazen “rebuild’i azalttım” sanırken asıl sorun paint tarafında kalabilir.

Rebuild işaretleri: “Ne kadar sık” ve “ne kadar geniş”?

Profiling sırasında iki soruya yanıt arayın: Hangi etkileşim rebuild’i tetikliyor ve rebuild hangi widget alt ağacına yayılıyor? Ardından build içinde yapılan işleri inceleyin: gereksiz map/filter, gereksiz widget üretimi, yanlış key, kontrolsüz dinleme gibi ipuçları genelde görünür hale gelir.

Performans analizi sırasında frame sürelerinin ve rebuild etkisinin izini sürmeye odaklanan geliştirici masası ve ekran görünümü

Uygulamada kontrol listesi: En sık işe yarayan adımlar

Rebuild optimizasyonu, birkaç güvenilir alışkanlığın toplamıdır. Aşağıdaki kontrol listesi, çoğu projede hızlı kazanım sağlar; özellikle liste-heavy ekranlarda ve sık etkileşimli UI’larda etkisi belirginleşir.

  • Build metodunu hafif tut: Pahalı hesaplamaları önceden hazırla, cache’le veya ayrı katmana taşı.
  • State’i böl: Tek bir dev StatefulWidget yerine, state’i ihtiyaca göre küçük bileşenlere dağıt.
  • const kullan: Statik parçaları const yaparak gereksiz nesne üretimini azalt.
  • Seçici dinle: Tüm modeli değil, sadece gereken alanı izleyen yaklaşımları tercih et.
  • Key stratejisini sade tut: Rastgele key üretmek yerine veri kimliğiyle uyumlu, stabil key kullan.
  • Ölç ve doğrula: DevTools Timeline ile önce sorunun kaynağını kesinleştir.

Sonuç: Rebuild’i yönet, performansı stabilize et

Flutter’da akıcı performans, rebuild’i tamamen “yok etmekten” değil; rebuild’in etkisini doğru sınırlamaktan geçer. Yapıyı küçük parçalara ayırmak, const ile stabil UI oluşturmak, state değişimini seçici hale getirmek ve build içindeki işleri hafifletmek; birlikte çalıştığında güçlü bir fark yaratır.

Unutmayın: Her optimizasyon bir maliyet taşır. Bu yüzden önce doğru sorunu bulun, sonra doğru yerde düzeltin. Böylece kod tabanınız bakımı kolay kalır ve kullanıcılarınız daha akıcı bir deneyim yaşar.

 VERİ AKADEMİ