How do you handle performance optimization - app size, rendering speed, and load time?

#performance

Answer

Overview

Flutter performance optimization spans three key areas: app size, rendering speed, and load time. Each requires a different approach.


1. Rendering Speed (60/120fps)

Use const Widgets

dart
// ✅ const widget is cached — never rebuilt
const Text('Static Label')
const SizedBox(height: 16)
const Icon(Icons.star)

// ❌ Non-const widget rebuilds unnecessarily
Text('Static Label') // Rebuilt on every setState

Avoid Unnecessary Rebuilds

dart
// ❌ Rebuilds entire tree on counter change
class MyWidget extends StatefulWidget {
  
  Widget build(BuildContext context) {
    return Column(
      children: [
        ExpensiveHeader(), // Rebuilt even though counter doesn't affect it
        Text('$_counter'),
      ],
    );
  }
}

// ✅ Only rebuild what changed
Column(
  children: [
    const ExpensiveHeader(), // const — never rebuilt
    Consumer<CounterModel>(
      builder: (context, model, child) => Text('${model.count}'),
    ),
  ],
)

Use ListView.builder (Not ListView) — Lazy Loading

text
ListView.builder
is lazy by design — it only builds widgets for items currently visible on screen. Items outside the viewport are never built, and items that scroll off-screen are destroyed. This is true lazy loading.

dart
// ❌ Eager loading — builds ALL 1000 items at once, even off-screen
ListView(
  children: items.map((e) => ItemWidget(e)).toList(), // 1000 widgets created immediately
)

// ✅ Lazy loading — builds only ~10-15 visible items at a time
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    // Called ONLY when item[index] is about to become visible
    return ItemWidget(items[index]);
  },
)

How lazy loading works in ListView.builder:

  • Viewport renders only the visible items + a small buffer
  • As user scrolls down, new items are built on demand
  • Items that scroll off-screen are disposed to free memory
  • text
    itemBuilder
    is called just-in-time — never for off-screen items

Lazy Loading with Pagination (Infinite Scroll)

For loading data from an API as the user scrolls, combine

text
ListView.builder
with a
text
ScrollController
:

dart
class ProductListScreen extends StatefulWidget {
  
  State<ProductListScreen> createState() => _ProductListScreenState();
}

class _ProductListScreenState extends State<ProductListScreen> {
  final ScrollController _scrollController = ScrollController();
  final List<Product> _products = [];
  bool _isLoading = false;
  bool _hasMore = true;
  int _page = 1;

  
  void initState() {
    super.initState();
    _fetchProducts(); // Load first page
    _scrollController.addListener(_onScroll);
  }

  void _onScroll() {
    // Trigger fetch when user is 200px from the bottom
    final atBottom = _scrollController.position.pixels >=
        _scrollController.position.maxScrollExtent - 200;

    if (atBottom && !_isLoading && _hasMore) {
      _fetchProducts();
    }
  }

  Future<void> _fetchProducts() async {
    setState(() => _isLoading = true);

    final newProducts = await ProductApi.fetchPage(page: _page);

    setState(() {
      _products.addAll(newProducts);
      _isLoading = false;
      _hasMore = newProducts.isNotEmpty;
      _page++;
    });
  }

  
  void dispose() {
    _scrollController.dispose(); // Always dispose to prevent memory leaks
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _scrollController,
      itemCount: _products.length + (_hasMore ? 1 : 0),
      itemBuilder: (context, index) {
        // Last item — show loading spinner while fetching next page
        if (index == _products.length) {
          return const Center(
            child: Padding(
              padding: EdgeInsets.all(16),
              child: CircularProgressIndicator(),
            ),
          );
        }
        return ProductCard(_products[index]);
      },
    );
  }
}

Avoid Expensive Operations in build()

dart
// ❌ Sorting inside build — runs every rebuild
Widget build(BuildContext context) {
  final sorted = items.sort(...); // Expensive!
  return ListView.builder(...);
}

// ✅ Pre-compute and cache
final _sortedItems = [...items]..sort(...); // Computed once

Widget build(BuildContext context) {
  return ListView.builder(itemCount: _sortedItems.length, ...);
}

Use RepaintBoundary for Complex Widgets

dart
RepaintBoundary(
  child: ComplexAnimatedChart(), // Only this layer repaints, not the whole tree
)

2. App Size Reduction

bash
# Build with size analysis
flutter build apk --analyze-size
flutter build ios --analyze-size

# Use --split-per-abi to reduce APK size
flutter build apk --split-per-abi
# Produces: arm64-v8a, armeabi-v7a, x86_64 APKs separately

Remove Unused Assets

yaml
# pubspec.yaml — only include what you use
flutter:
  assets:
    - assets/images/logo.png  # ✅ Only specific files
    # - assets/images/       # ❌ Avoid including entire folders

Use WebP images instead of PNG/JPG

dart
// Use WebP format — 25-35% smaller than PNG
Image.asset('assets/images/photo.webp')

Defer unused packages / tree shake

Dart automatically tree-shakes unused code in release builds. Avoid heavy packages for simple tasks.


3. Load Time Optimization

Defer Heavy Initialization

dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(MyApp()); // Show UI immediately

  // Defer non-critical initializations
  WidgetsBinding.instance.addPostFrameCallback((_) async {
    await FirebaseAnalytics.init();
    await RemoteConfig.fetchAndActivate();
  });
}

Cache Network Images

dart
// ✅ Use cached_network_image — avoids re-downloading
CachedNetworkImage(
  imageUrl: url,
  placeholder: (context, url) => Shimmer(),
  errorWidget: (context, url, error) => Icon(Icons.error),
)

Use Isolate for Heavy Parsing

dart
// Parse large JSON in background
Future<List<Product>> loadProducts(String jsonStr) async {
  return await compute(_parseProducts, jsonStr);
}

List<Product> _parseProducts(String json) {
  return (jsonDecode(json) as List).map((e) => Product.fromJson(e)).toList();
}

Performance Checklist

OptimizationImpact
text
const
widgets
⭐⭐⭐ High
text
ListView.builder
⭐⭐⭐ High
Image caching⭐⭐⭐ High
text
--split-per-abi
⭐⭐ Medium
text
RepaintBoundary
⭐⭐ Medium
Avoid build() logic⭐⭐ Medium
Isolate for heavy work⭐⭐ Medium
Defer initialization⭐⭐ Medium
Use WebP images⭐ Low-Medium

Profiling Tools

bash
# Profile mode — runs with production performance but debug tools available
flutter run --profile

# Open DevTools for frame timings, rebuild counts, memory
flutter pub global run devtools

Golden Rules: Use

text
const
wherever possible. Never do heavy work in
text
build()
. Use
text
ListView.builder
for lists. Cache images. Defer non-critical initialization.