Skip to content

[framework] [scrolling] [performance] ScrollController abnormal behavior with long lists #153085

@iapicca

Description

@iapicca

Related: #52207


I'm opening this issue because as follow up on the issue below

the behavior of ScrollController shows some anomalies, please follow the steps below

Steps to reproduce

  1. run in release mode the code sample provided on android device (likely other platform experience the same issue)

  2. with only text in the list
    a. tap the fab ~163s and observe fair performances
    b. tap the fab ~5s and observe poor performances
    c. tap the fab reset and observe bad performances

  3. tap the flutter logo to change the content of the list

  4. with flutter logo in the list
    a. tap the fab ~163s and observe fair performances
    b. tap the fab ~5s and observe very bad performances
    . (a lot of time between the end of the scrolling and the snackbar, see video)
    c. tap the fab reset and observe catastrophic performances (sometimes freezes the app permanently)

Expected results

  • ScrollViewController should handle "fast animations" and "jumps" more gracefully
  • the time set in animateTo should be "respected"
  • animateTo should return immediately after the scrolling is over
  • jumpTo shouldn't freeze the app

Actual results

Code sample

Code sample
import 'package:flutter/material.dart';

/// roughly a frame at 60 fps
const _microsecondsPerFrameAt60fps = 16333;
const _arbitraryScrollDuration = Duration(seconds: 5);
const _itemCount = 10000;
const _sixtyFPSDuration = Duration(
  microseconds: _microsecondsPerFrameAt60fps * _itemCount,
);

void main() => runApp(
      const MaterialApp(
        showPerformanceOverlay: true,
        home: MyHomePage(),
      ),
    );

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  var _hasIcon = false;
  late final ScrollController _controller;

  @override
  void initState() {
    super.initState();
    _controller = ScrollController();
  }

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

  void _toggleIcon() => setState(() => _hasIcon = !_hasIcon);

  @override
  Widget build(context) => Scaffold(
        body: ListView.builder(
          controller: _controller,
          itemCount: _itemCount,
          itemBuilder: (context, index) {
            final textWidget = Expanded(
              flex: 1,
              child: Text('$index', textAlign: TextAlign.center),
            );
            return Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [
                textWidget,
                for (var i = 0; i < 5; i++) ...[
                  _hasIcon
                      ? const Expanded(
                          flex: 1,
                          child: FlutterLogo(),
                        )
                      : textWidget,
                ]
              ],
            );
          },
        ),
        floatingActionButton: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            FloatingActionButton(
              onPressed: () => _controller.jumpTo(0),
              child: const Text('reset'),
            ),
            TimerButton(
              controller: _controller,
              duration: _arbitraryScrollDuration,
            ),
            TimerButton(
              controller: _controller,
              duration: _sixtyFPSDuration,
            ),
            FloatingActionButton(
              onPressed: _toggleIcon,
              child: _hasIcon
                  ? const Icon(Icons.text_increase_outlined)
                  : const FlutterLogo(),
            ),
          ],
        ),
      );
}

class TimerButton extends StatelessWidget {
  final ScrollController controller;
  final Duration duration;
  const TimerButton({
    super.key,
    required this.controller,
    required this.duration,
  });

  @override
  Widget build(context) => FloatingActionButton(
        onPressed: () async {
          final start = DateTime.now();

          await controller.animateTo(
            controller.position.maxScrollExtent,
            duration: duration,
            curve: Curves.linear,
          );

          if (!context.mounted) {
            return;
          }

          final actualDuration = DateTime.now().difference(start);
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('expected duration: ${duration.inMilliseconds} ms\n'
                  'actual duration: ${actualDuration.inMilliseconds}'),
            ),
          );
        },
        child: Text('~${duration.inSeconds} s'),
      );
}

Screenshots or Video

Screenshots / Video demonstration
video_sample.mp4

Logs

No response

Flutter Doctor output

Doctor output
flutter doctor -v
[✓] Flutter (Channel stable, 3.24.0, on macOS 14.5 23F79 darwin-arm64, locale en-US)
    • Flutter version 3.24.0 on channel stable at /Users/francesco/fvm/versions/stable
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 80c2e84975 (9 days ago), 2024-07-30 23:06:49 +0700
    • Engine revision b8800d88be
    • Dart version 3.5.0
    • DevTools version 2.37.2

[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    • Android SDK at /Users/francesco/Library/Android/sdk
    • Platform android-34, build-tools 34.0.0
    • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b829.9-10027231)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 15.4)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 15F31d
    • CocoaPods version 1.15.2

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2022.3)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b829.9-10027231)

[✓] VS Code (version 1.92.0)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.94.0

[✓] Connected device (4 available)
    • Pixel 3a (mobile)               • 965AY0WP5C            • android-arm64  • Android 12 (API 32)
    • macOS (desktop)                 • macos                 • darwin-arm64   • macOS 14.5 23F79 darwin-arm64
    • Mac Designed for iPad (desktop) • mac-designed-for-ipad • darwin         • macOS 14.5 23F79 darwin-arm64
    • Chrome (web)                    • chrome                • web-javascript • Google Chrome 120.0.6099.71

[✓] Network resources
    • All expected network resources are available.

• No issues found!

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listc: performanceRelates to speed or footprint issues (see "perf:" labels)f: scrollingViewports, list views, slivers, etc.found in release: 3.24Found to occur in 3.24frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onperf: memoryPerformance issues related to memoryr: duplicateIssue is closed as a duplicate of an existing issueteam-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions