Flutter'da Performance Optimizasyon Teknikleri
Flutter genellikle 60 veya 120 fps'de sorunsuz çalışır; ancak yanlış kullanım kalıpları yeniden çizimleri ve gereksiz build'leri tetikleyerek uygulamayı yavaşlatır. Bu yazıda ölçülebilir fark yaratan optimizasyon tekniklerini ele alıyoruz.
const Kullanımı: En Kolay Optimizasyon
Bir widget const olduğunda Flutter onu bir kez oluşturur ve üst widget yeniden build edilse bile bu widget'ı atlar:
// KÖTÜ — her build'de yeni instance
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: EdgeInsets.all(16), // her seferinde yeni EdgeInsets
child: Text('Başlık', style: TextStyle(fontSize: 20)),
),
Icon(Icons.star, color: Colors.amber),
],
);
}
// İYİ — const widget'lar atlanır
Widget build(BuildContext context) {
return const Column(
children: [
Padding(
padding: EdgeInsets.all(16),
child: Text('Başlık', style: TextStyle(fontSize: 20)),
),
Icon(Icons.star, color: Colors.amber),
],
);
}
// Kuralı basit tut: statik içerik varsa const ekle
// flutter analyze --no-preamble kural ihlallerini gösterir
// pubspec.yaml → analyzer → rules: prefer_const_constructors: true
ListView Optimizasyonu: Builder Kullanın
// KÖTÜ — tüm item'lar aynı anda build edilir ve bellekte tutulur
ListView(
children: products.map((p) => ProductCard(product: p)).toList(),
)
// İYİ — sadece görünen item'lar build edilir
ListView.builder(
itemCount: products.length,
itemExtent: 120, // bilinen yükseklik performansı artırır
itemBuilder: (context, index) => ProductCard(product: products[index]),
)
// Büyük listeler için itemExtent veya prototypeItem zorunlu
ListView.builder(
prototypeItem: const ProductCard(product: Product.placeholder),
itemCount: products.length,
itemBuilder: (context, index) => ProductCard(product: products[index]),
)
// Sliver versiyonu — CustomScrollView içinde
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ProductCard(product: products[index]),
childCount: products.length,
),
)
setState Kapsamını Küçültmek
// KÖTÜ — küçük değişiklik tüm sayfayı yeniden build eder
class ProductPageBad extends StatefulWidget { ... }
class _State extends State<ProductPageBad> {
bool _isFavorite = false;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
// Büyük ve pahalı liste widget'ı
const HeavyProductList(),
// Sadece bu değişiyor ama tüm sayfa rebuild oluyor
IconButton(
icon: Icon(_isFavorite ? Icons.favorite : Icons.favorite_border),
onPressed: () => setState(() => _isFavorite = !_isFavorite),
),
],
),
);
}
}
// İYİ — setState kapsamı küçük bir widget'a izole edilir
class FavoriteButton extends StatefulWidget {
const FavoriteButton({super.key});
@override State<FavoriteButton> createState() => _FavoriteButtonState();
}
class _FavoriteButtonState extends State<FavoriteButton> {
bool _isFavorite = false;
@override
Widget build(BuildContext context) => IconButton(
icon: Icon(_isFavorite ? Icons.favorite : Icons.favorite_border,
color: _isFavorite ? Colors.red : null),
onPressed: () => setState(() => _isFavorite = !_isFavorite),
);
}
// Sayfa — HeavyProductList hiç rebuild olmaz
class ProductPage extends StatelessWidget {
const ProductPage({super.key});
@override
Widget build(BuildContext context) => const Scaffold(
body: Column(children: [HeavyProductList(), FavoriteButton()]),
);
}
RepaintBoundary: Yeniden Çizim Sınırı
// Animasyonlu bir widget sık sık repaint oluyor ama etrafını etkilememeli
class AnimatedCounter extends StatelessWidget {
final int count;
const AnimatedCounter({super.key, required this.count});
@override
Widget build(BuildContext context) {
return RepaintBoundary( // Bu alt ağacı ayrı bir layer'da tut
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: count.toDouble()),
duration: const Duration(milliseconds: 500),
builder: (context, value, _) => Text(
value.round().toString(),
style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
),
),
);
}
}
// Hangi widget'lara RepaintBoundary koymalı?
// - Sürekli animasyon içerenler
// - Sıkça güncellenen ama büyük bir ağacın küçük parçası olanlar
// - Kompleks custom painter'lar
// DevTools'daki "Highlight Repaints" özelliğiyle doğrulayın
compute() ile Ağır İşlemleri Isolate'e Taşımak
// UI thread'i bloke eden işlemler jank yaratır
// KÖTÜ — UI thread'de JSON parse etmek
Future<List<Product>> parseProducts(String jsonString) async {
final list = jsonDecode(jsonString) as List; // büyük JSON'da donma
return list.map((e) => Product.fromJson(e)).toList();
}
// İYİ — ayrı isolate'de çalıştır
Future<List<Product>> parseProductsInBackground(String jsonString) {
return compute(_parseProductsIsolate, jsonString);
}
// Top-level fonksiyon olmalı
List<Product> _parseProductsIsolate(String jsonString) {
final list = jsonDecode(jsonString) as List;
return list.map((e) => Product.fromJson(e as Map<String, dynamic>)).toList();
}
// Kullanım — loading gösterirken arka planda parse et
Future<void> _loadProducts() async {
final response = await apiClient.get('/products');
final products = await parseProductsInBackground(response.data);
emit(ProductsLoaded(products));
}
// compute() için uygun durumlar:
// - 50ms+ süren JSON parse işlemleri
// - Görüntü işleme
// - Şifreleme/hash işlemleri
// - Büyük liste sıralama/filtreleme
Image Önbellekleme
// cached_network_image: ^3.4.1
CachedNetworkImage(
imageUrl: product.imageUrl,
placeholder: (_, __) => const ShimmerPlaceholder(),
errorWidget: (_, __, ___) => const Icon(Icons.broken_image),
memCacheWidth: 300, // bellek kullanımını sınırla
memCacheHeight: 300,
)
// Image boyutlarını widget boyutuna göre ayarla
Image.network(
url,
width: 100, height: 100,
fit: BoxFit.cover,
cacheWidth: 200, // 2x density için
)
Optimizasyona her zaman ölçüm ile başlayın. Flutter DevTools'un Performance ve CPU Profiler sekmelerini kullanarak gerçek darboğazları bulun. const eklemek ve ListView.builder kullanmak her projede işe yarar; RepaintBoundary ve isolate ise ölçümle doğrulandıktan sonra uygulanmalıdır.