Back to Posts

Fixing Scroll Performance Issues in Flutter

8 min read

Scroll performance issues can significantly impact your Flutter application's user experience. This comprehensive guide covers everything from basic scroll optimization to advanced performance techniques and memory management.

Understanding Scroll Performance

1. Scroll Performance Components

Flutter's scroll performance involves:

  • List rendering
  • Memory management
  • Frame timing
  • Widget rebuilding
  • Layout calculations

2. Scroll Performance Monitor

class ScrollPerformanceMonitor extends StatelessWidget {
  final Widget child;
  final String? tag;

  const ScrollPerformanceMonitor({
    required this.child,
    this.tag,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return PerformanceOverlay(
      optionsMask: PerformanceOverlayOption.all,
      rasterizerThreshold: 0,
      checkerboardRasterCacheImages: true,
      checkerboardOffscreenLayers: true,
      child: child,
    );
  }
}

Common Scroll Issues and Solutions

1. List View Optimization

class OptimizedListView extends StatelessWidget {
  final List<Widget> children;
  final ScrollController? controller;
  final EdgeInsets? padding;

  const OptimizedListView({
    required this.children,
    this.controller,
    this.padding,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: controller,
      padding: padding,
      itemCount: children.length,
      itemBuilder: (context, index) {
        return RepaintBoundary(
          child: children[index],
        );
      },
      addAutomaticKeepAlives: false,
      addRepaintBoundaries: false,
    );
  }
}

2. Grid View Optimization

class OptimizedGridView extends StatelessWidget {
  final List<Widget> children;
  final int crossAxisCount;
  final double mainAxisSpacing;
  final double crossAxisSpacing;
  final ScrollController? controller;

  const OptimizedGridView({
    required this.children,
    this.crossAxisCount = 2,
    this.mainAxisSpacing = 0,
    this.crossAxisSpacing = 0,
    this.controller,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      controller: controller,
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: crossAxisCount,
        mainAxisSpacing: mainAxisSpacing,
        crossAxisSpacing: crossAxisSpacing,
      ),
      itemCount: children.length,
      itemBuilder: (context, index) {
        return RepaintBoundary(
          child: children[index],
        );
      },
      addAutomaticKeepAlives: false,
      addRepaintBoundaries: false,
    );
  }
}

3. Scroll Controller Management

class ScrollControllerManager {
  static final Map<String, ScrollController> _controllers = {};

  static ScrollController getController(String key) {
    if (!_controllers.containsKey(key)) {
      _controllers[key] = ScrollController();
    }
    return _controllers[key]!;
  }

  static void disposeController(String key) {
    _controllers[key]?.dispose();
    _controllers.remove(key);
  }

  static void disposeAll() {
    _controllers.values.forEach((controller) => controller.dispose());
    _controllers.clear();
  }
}

Advanced Scroll Management

1. Lazy Loading

class LazyLoadingList extends StatefulWidget {
  final int itemCount;
  final Widget Function(BuildContext, int) itemBuilder;
  final Future<void> Function() onLoadMore;
  final ScrollController? controller;

  const LazyLoadingList({
    required this.itemCount,
    required this.itemBuilder,
    required this.onLoadMore,
    this.controller,
    Key? key,
  }) : super(key: key);

  @override
  _LazyLoadingListState createState() => _LazyLoadingListState();
}

class _LazyLoadingListState extends State<LazyLoadingList> {
  bool _isLoading = false;

  @override
  void initState() {
    super.initState();
    widget.controller?.addListener(_onScroll);
  }

  @override
  void dispose() {
    widget.controller?.removeListener(_onScroll);
    super.dispose();
  }

  void _onScroll() {
    if (_isLoading) return;

    final maxScroll = widget.controller!.position.maxScrollExtent;
    final currentScroll = widget.controller!.position.pixels;
    final threshold = maxScroll * 0.8;

    if (currentScroll >= threshold) {
      _loadMore();
    }
  }

  Future<void> _loadMore() async {
    if (_isLoading) return;

    setState(() => _isLoading = true);
    await widget.onLoadMore();
    setState(() => _isLoading = false);
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: widget.controller,
      itemCount: widget.itemCount + (_isLoading ? 1 : 0),
      itemBuilder: (context, index) {
        if (index == widget.itemCount) {
          return const Center(child: CircularProgressIndicator());
        }
        return widget.itemBuilder(context, index);
      },
    );
  }
}

2. Scroll Physics Customization

class CustomScrollPhysics extends ScrollPhysics {
  final double? dragStartDistanceMotionThreshold;
  final double? velocityThreshold;
  final double? minFlingVelocity;
  final double? maxFlingVelocity;

  const CustomScrollPhysics({
    this.dragStartDistanceMotionThreshold,
    this.velocityThreshold,
    this.minFlingVelocity,
    this.maxFlingVelocity,
    ScrollPhysics? parent,
  }) : super(parent: parent);

  @override
  CustomScrollPhysics applyTo(ScrollPhysics? ancestor) {
    return CustomScrollPhysics(
      dragStartDistanceMotionThreshold: dragStartDistanceMotionThreshold,
      velocityThreshold: velocityThreshold,
      minFlingVelocity: minFlingVelocity,
      maxFlingVelocity: maxFlingVelocity,
      parent: buildParent(ancestor),
    );
  }

  @override
  double get dragStartDistanceMotionThreshold =>
      this.dragStartDistanceMotionThreshold ?? super.dragStartDistanceMotionThreshold;

  @override
  double get velocityThreshold => this.velocityThreshold ?? super.velocityThreshold;

  @override
  double get minFlingVelocity => this.minFlingVelocity ?? super.minFlingVelocity;

  @override
  double get maxFlingVelocity => this.maxFlingVelocity ?? super.maxFlingVelocity;
}

Performance Optimization

1. Memory Management

class ScrollMemoryManager {
  static final Map<String, List<Widget>> _cache = {};

  static List<Widget> getCachedItems(String key) {
    return _cache[key] ?? [];
  }

  static void cacheItems(String key, List<Widget> items) {
    _cache[key] = items;
  }

  static void clearCache() {
    _cache.clear();
  }

  static void removeFromCache(String key) {
    _cache.remove(key);
  }
}

2. Widget Recycling

class RecycledListView extends StatefulWidget {
  final int itemCount;
  final Widget Function(BuildContext, int) itemBuilder;
  final ScrollController? controller;

  const RecycledListView({
    required this.itemCount,
    required this.itemBuilder,
    this.controller,
    Key? key,
  }) : super(key: key);

  @override
  _RecycledListViewState createState() => _RecycledListViewState();
}

class _RecycledListViewState extends State<RecycledListView> {
  final Map<int, Widget> _recycledWidgets = {};

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: widget.controller,
      itemCount: widget.itemCount,
      itemBuilder: (context, index) {
        if (!_recycledWidgets.containsKey(index)) {
          _recycledWidgets[index] = widget.itemBuilder(context, index);
        }
        return _recycledWidgets[index]!;
      },
    );
  }
}

Testing and Debugging

1. Scroll Performance Tests

void main() {
  testWidgets('Scroll Performance Test', (WidgetTester tester) async {
    final controller = ScrollController();
    final items = List.generate(100, (index) => Text('Item $index'));

    await tester.pumpWidget(
      MaterialApp(
        home: ScrollPerformanceMonitor(
          child: OptimizedListView(
            controller: controller,
            children: items,
          ),
        ),
      ),
    );

    await tester.fling(
      find.byType(ListView),
      const Offset(0, -500),
      1000,
    );

    await tester.pumpAndSettle();
    // Additional assertions
  });
}

2. Memory Usage Tests

void main() {
  test('Memory Usage Test', () async {
    final items = List.generate(1000, (index) => Text('Item $index'));
    ScrollMemoryManager.cacheItems('test', items);

    expect(ScrollMemoryManager.getCachedItems('test').length, equals(1000));

    ScrollMemoryManager.clearCache();
    expect(ScrollMemoryManager.getCachedItems('test').length, equals(0));
  });
}

Best Practices

  1. Use ListView.builder: Implement efficient list rendering
  2. Implement Lazy Loading: Load content as needed
  3. Optimize Widget Rebuilding: Use const constructors and RepaintBoundary
  4. Manage Memory: Clear unused resources
  5. Customize Scroll Physics: Adjust scroll behavior for better performance
  6. Monitor Performance: Track scroll performance metrics
  7. Implement Caching: Cache frequently accessed items
  8. Test Across Devices: Verify performance on different devices

Conclusion

Effective scroll performance optimization in Flutter requires:

  • Proper list implementation
  • Memory management
  • Performance monitoring
  • Comprehensive testing
  • Implementation of best practices

By following these guidelines and implementing the provided solutions, you can ensure smooth and efficient scrolling in your Flutter application.