Flutter: Performance Optimization

Performance is the baseline of user experience — 60fps is a promise, 59fps is a failure.

📋 Prerequisites: You should have mastered the following first

1. What You Will Learn


2. A Real Story of Jank

(1) The Pain: Product List Scrolling at 30fps

Bob's ShopApp product list scrolls smoothly at 1000 items, but drops to 30fps at 10000 items. DevTools reveals: every product card triggers a rebuild during scrolling because the parent Widget's setState causes the entire subtree to rebuild. Worse, images have no caching — every time an item scrolls into the viewport, it re-downloads, and memory usage spikes to 500MB.

(2) The Solution: Targeted Optimization

Performance optimization is not "optimize everything" — it's finding bottlenecks and targeting them precisely. DevTools pinpoints the problem → RepaintBoundary isolates → const reduces rebuilds → image caching reduces network calls.

DART
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';

// Dependency: cached_network_image: ^3.0.0 (add to dependencies)
// Product comes from the ShopApp project, full definition in Lesson 26

// Key optimizations
const ProductCard(...);                      // 1. const avoids rebuild
RepaintBoundary(child: Image.network(...));  // 2. Isolate repaint
CachedNetworkImage(imageUrl: ...);           // 3. Cache images
AutomaticKeepAliveClientMixin;               // 4. Keep tab state
TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

(3) The Result: Million-level 60fps + Memory Halved

After targeted optimization, Bob's million-product list maintains a stable 60fps, memory drops from 500MB to 200MB, and image cache hit rate reaches 95%.


3. Performance Optimization Overview

100%
graph TD
    PO[Performance Optimization] --> DEV[DevTools]
    PO --> RO[Render Optimization]
    PO --> MO[Memory Optimization]
    PO --> NO[Network Optimization]
    RO --> |const| CW[const Widgets]
    RO --> |RepaintBoundary| RB2[Isolate Repaint]
    MO --> |cache| IMG[Image Cache]
    NO --> |pagination| PG[Pagination API]
    DEV --> |Overlay| FPS[FPS Monitor]
TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

4. DevTools Performance Analysis

(1) Core DevTools

Tool Purpose How to Use
Performance Overlay Real-time FPS + GPU thread time debugShowPerformanceOverlay: true
Widget Rebuild Tracker Track which Widgets rebuild frequently DevTools → Rebuild Tracker
CPU Profiler Analyze CPU hotspots DevTools → CPU Profiler
Memory Chart Memory allocation & GC DevTools → Memory
Network Inspector Network request waterfall DevTools → Network
Flutter Inspector Widget tree visualization DevTools → Inspector

▶ Example

TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

: Enabling performance analysis

DART
import 'package:flutter/material.dart';

// Development mode: enable performance overlay
MaterialApp(
  debugShowPerformanceOverlay: true,
  showSemanticsDebugger: false,
  // ...
)

// Profile mode: accurate performance measurement
// Run: flutter run --profile
// Then open DevTools: flutter pub global run devtools
TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
Run Mode Optimization Purpose
Debug No optimization Development & debugging
Profile Near-production Performance analysis
Release Fully optimized Production release

5. Render Optimization

(1) const Constructors

Comparison Non-const const
Rebuild behavior Creates a new instance each time Reuses the same instance
canUpdate May not pass Always passes
Performance impact Overhead from repeated rebuilds Zero rebuild overhead

▶ Example

TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

: Const-optimized product card

DART
import 'package:flutter/material.dart';

// BAD: not const, rebuilds every time
Widget build(BuildContext context) {
  return Padding(
    padding: EdgeInsets.all(8.0), // New instance each build
    child: Text('Price: \$${product.price}'),
  );
}

// GOOD: const where possible
Widget build(BuildContext context) {
  return const Padding(  // const padding
    padding: EdgeInsets.all(8.0), // Compile-time constant
    child: Text('Fixed text'),    // const if content is constant
  );
}

// For dynamic content, const the wrapper
Widget build(BuildContext context) {
  return const Padding(
    padding: EdgeInsets.all(8.0),
    child: _DynamicPrice(product: product),  // Can't const dynamic
  );
}
TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

▶ Example

TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

: RepaintBoundary to isolate repaints

DART
import 'package:flutter/material.dart';

// Product comes from the ShopApp project, full definition in Lesson 26

// Without RepaintBoundary: entire list repaints when one item changes
ListView.builder(
  itemCount: 1000,
  itemBuilder: (_, i) => ProductCard(product: products[i]),
)

// With RepaintBoundary: only changed card repaints
ListView.builder(
  itemCount: 1000,
  itemBuilder: (_, i) => RepaintBoundary(
    key: ValueKey(products[i].id),
    child: ProductCard(product: products[i]),
  ),
)
TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

(2) Avoiding Large Widget Rebuilds

Problem Solution
Large build method rebuilds entirely Split into multiple smaller Widgets
setState rebuilds the entire subtree Only rebuild the affected part
Parent Widget changes affect subtree Use const child components to block propagation

▶ Example

TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

: Splitting a large Widget

DART
import 'package:flutter/material.dart';

// BAD: entire page rebuilds on counter change
class BadPage extends StatefulWidget {
  @override
  State<BadPage> createState() => _BadPageState();
}

class _BadPageState extends State<BadPage> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(children: [
      const HeavyHeader(),       // Rebuilds unnecessarily!
      const ProductGrid(),       // Rebuilds unnecessarily!
      Text('Count: $_count'),    // Only this needs rebuild
      ElevatedButton(onPressed: () => setState(() => _count++), child: const Text('+')),
    ]);
  }
}

// GOOD: only counter part rebuilds
class GoodPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(children: [
      const HeavyHeader(),       // Never rebuilds (const)
      const ProductGrid(),       // Never rebuilds (const)
      const _CounterSection(),   // Only this rebuilds
    ]);
  }
}

// HeavyHeader / ProductGrid are placeholder example Widgets
class HeavyHeader extends StatelessWidget {
  const HeavyHeader({super.key});
  @override Widget build(BuildContext context) => const Text('Header');
}
class ProductGrid extends StatelessWidget {
  const ProductGrid({super.key});
  @override Widget build(BuildContext context) => const Text('Grid');
}

class _CounterSection extends StatefulWidget {
  const _CounterSection();
  @override
  State<_CounterSection> createState() => _CounterSectionState();
}

class _CounterSectionState extends State<_CounterSection> {
  int _count = 0;
  @override
  Widget build(BuildContext context) {
    return Column(children: [
      Text('Count: $_count'),
      ElevatedButton(onPressed: () => setState(() => _count++), child: const Text('+')),
    ]);
  }
}
TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

6. Memory Optimization

▶ Example

TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

: AutomaticKeepAliveClientMixin

DART
import 'package:flutter/material.dart';

// Product / ProductTile come from the ShopApp project, full definitions in Lessons 26/27

// Keep tab content alive when switching tabs
class ProductListTab extends StatefulWidget {
  @override
  State<ProductListTab> createState() => _ProductListTabState();
}

class _ProductListTabState extends State<ProductListTab>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true; // Keep state when tab is hidden

  @override
  Widget build(BuildContext context) {
    super.build(context); // Required for mixin
    return ListView.builder(
      itemCount: products.length,
      itemBuilder: (_, i) => ProductTile(product: products[i]),
    );
  }
}
TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

▶ Example

TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

: Dispose resource release

DART
import 'dart:async';
import 'package:flutter/material.dart';

// DetailPage / productStream come from the ShopApp project

class _DetailPageState extends State<DetailPage> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late TextEditingController _textController;
  StreamSubscription? _subscription;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(seconds: 1));
    _textController = TextEditingController();
    _subscription = productStream.listen((_) => setState(() {}));
  }

  @override
  void dispose() {
    _controller.dispose();    // Release animation
    _textController.dispose(); // Release text controller
    _subscription?.cancel();   // Cancel stream
    super.dispose();
  }
}
TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

7. Network Optimization

▶ Example

TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

: cached_network_image for image caching

DART
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';

// Dependency: cached_network_image: ^3.0.0 (add to dependencies)
// product.imageUrl comes from ShopApp Product entity

CachedNetworkImage(
  imageUrl: product.imageUrl,
  placeholder: (context, url) => Container(
    color: Colors.grey[200],
    child: const Center(child: CircularProgressIndicator()),
  ),
  errorWidget: (context, url, error) => const Icon(Icons.error),
  fadeInDuration: const Duration(milliseconds: 300),
  memCacheWidth: 400, // Resize in memory to save RAM
  maxWidth: 800,      // Disk cache max width
)
TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

▶ Example

TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

: Paginated API + preloading

DART
import 'package:flutter/material.dart';

// Dependency: no extra dependencies (Dart/Flutter standard library sufficient)
// Product / ProductTile / LoadingIndicator come from the ShopApp project
// api.fetchProducts comes from the ShopApp network layer

class PaginatedProductList extends StatefulWidget {
  @override
  State<PaginatedProductList> createState() => _PaginatedProductListState();
}

class _PaginatedProductListState extends State<PaginatedProductList> {
  final _controller = ScrollController();
  List<Product> _products = [];
  int _page = 1;
  bool _hasMore = true;

  @override
  void initState() {
    super.initState();
    _loadPage();
    _controller.addListener(() {
      // Preload when 2 screens away from bottom
      if (_controller.position.pixels >= _controller.position.maxScrollExtent * 0.8) {
        _loadPage();
      }
    });
  }

  Future<void> _loadPage() async {
    if (!_hasMore) return;
    final newProducts = await api.fetchProducts(page: _page, limit: 20);
    setState(() {
      _products.addAll(newProducts);
      _page++;
      _hasMore = newProducts.length >= 20;
    });
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _controller,
      itemCount: _products.length + (_hasMore ? 1 : 0),
      itemExtent: 72, // Fixed height for performance
      itemBuilder: (_, i) {
        if (i == _products.length) return const LoadingIndicator();
        return ProductTile(product: _products[i]);
      },
    );
  }
}
TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

8. Complete Example: ShopApp High-Performance Product List

DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_network_image/cached_network_image.dart';

// Dependencies: flutter_riverpod: ^2.0.0, cached_network_image: ^3.0.0
// Product / productProvider / cartProvider come from the ShopApp project, full definitions in Lessons 26/27

class OptimizedProductGrid extends ConsumerWidget {
  const OptimizedProductGrid({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final productsAsync = ref.watch(productProvider);
    return productsAsync.when(
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (e, _) => ErrorWidget(e),
      data: (products) => GridView.builder(
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          mainAxisSpacing: 8,
          crossAxisSpacing: 8,
          childAspectRatio: 0.7,
        ),
        itemCount: products.length,
        itemBuilder: (_, i) => RepaintBoundary(
          key: ValueKey(products[i].id),
          child: _OptimizedProductCard(product: products[i]),
        ),
      ),
    );
  }
}

class _OptimizedProductCard extends ConsumerWidget {
  final Product product;
  const _OptimizedProductCard({required this.product});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Card(
      clipBehavior: Clip.antiAlias,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Expanded(
            flex: 3,
            child: RepaintBoundary(  // Isolate image repaint
              child: CachedNetworkImage(
                imageUrl: product.imageUrl,
                fit: BoxFit.cover,
                memCacheWidth: 200,  // Save memory
                placeholder: (_, __) => Container(color: Colors.grey[200]),
                errorWidget: (_, __, ___) => const Icon(Icons.image_not_supported),
              ),
            ),
          ),
          Expanded(
            flex: 2,
            child: Padding(
              padding: const EdgeInsets.all(8),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(product.name, maxLines: 1, overflow: TextOverflow.ellipsis),
                  const Spacer(),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text('\$${product.price.toStringAsFixed(2)}',
                        style: const TextStyle(color: Colors.green)),
                      IconButton(
                        icon: const Icon(Icons.add_shopping_cart, size: 18),
                        onPressed: () => ref.read(cartProvider.notifier).addItem(product),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}
TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

❓ FAQ

Q Should I use debug or profile mode for performance analysis?
A You must use profile mode. Debug mode includes many check assertions that make FPS readings inaccurate.
Q Should I add const wherever possible?
A Yes. const constructors are zero-cost optimizations — you can add them whenever all constructor parameters are compile-time constants.
Q Is there a downside to adding too many RepaintBoundaries?
A Yes. Each RepaintBoundary creates an independent paint layer, increasing memory usage. Only add them at isolation points where repainting is frequent.
Q Why does itemExtent improve performance?
A With fixed item heights, ListView doesn't need to measure each child — it can directly calculate offsets, making scrolling smoother.
Q What's the difference between CachedNetworkImage and Image.network?
A Image.network has no caching and re-downloads every time; CachedNetworkImage automatically provides disk + memory caching, and supports placeholder and error widgets.
Q How do I detect memory leaks?
A Use the DevTools Memory panel to view memory trends. If memory keeps growing and never drops back, there's likely a leak. Common causes: Controllers not disposed, Streams not cancelled.

📖 Summary


📝 Exercises

  1. Basic (Difficulty ⭐): Use the Widget Rebuild Tracker in DevTools to find frequently rebuilt Widgets, and optimize them with const.
  2. Intermediate (Difficulty ⭐⭐): Add RepaintBoundary + itemExtent to a product list, and compare FPS before and after optimization.
  3. Challenge (Difficulty ⭐⭐⭐): Implement a complete high-performance product list: const Widget + RepaintBoundary + CachedNetworkImage(memCacheWidth) + paginated preloading + AutomaticKeepAliveClientMixin, achieving stable 60fps with 10000 items in profile mode.

← Previous Lesson | Next Lesson →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏