Skip to content

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

Closed

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!

Activity

  1. darshankawar commented on Aug 9, 2024

    @darshankawar
    Member

    Thanks for the detailed report @iapicca
    I was able to replicate and see the same behavior upon running on device as well as emulator with latest versions.

    Stable: 3.24.0
    Master: 3.24.0-1.0.pre.525
    
  2. added
    frameworkflutter/packages/flutter repository. See also f: labels.
    c: performanceRelates to speed or footprint issues (see "perf:" labels)
    f: scrollingViewports, list views, slivers, etc.
    perf: memoryPerformance issues related to memory
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    and removed
    in triagePresently being triaged by the triage team
    on Aug 9, 2024
  3. Piinks commented on Aug 20, 2024

    @Piinks
    Contributor

    This seems like a duplicate of #52207

    • ScrollViewController handles "fast animations" and "jumps" poorly
    • the time set in animateTo in some cases is disregarded
    • occasionally animateTo returns long after the scrolling is over
    • jumpTo occasionally freeze the app

    This is because the list is trying to lay out all of the items that are flying by (or at least trying to) so quickly. Everything from the start to the destination must be built so we know what to show at the given location, and it is causing these performance issues. I think we can close this to refer to the #52207 ?

  4. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Aug 20, 2024
  5. iapicca commented on Aug 21, 2024

    @iapicca
    Author

    This is because the list is trying to lay out all of the items that are flying by (or at least trying to) so quickly. Everything from the start to the destination must be built so we know what to show at the given location, and it is causing these performance issues. I think we can close this to refer to the #52207 ?

    Hi @Piinks
    I updated the initial post delegating the performance issue to the issue you mentioned above
    and added a comment regarding the "freezing"

    Actual results

    I feel that the other 2 aspects are worth investigating and,
    as pointed out in the title, somehow related to ScrollController

    1. the time set in animateTo in some cases is disregarded

    this is self explanatory

    1. [...] animateTo returns long after the scrolling is over

    this is very strange in my opinion, as ScaffoldMessenger's showSnackBar
    is called much later than the time collected by actualDuration
    this "feels" unrelated to scrolling and more relevant to rendering (imho)
    this behavior is easily reproducible in the step 4.b (or 4.c if doesn't explode)

  6. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Aug 21, 2024
  7. Piinks commented on Aug 21, 2024

    @Piinks
    Contributor

    IMO it is not actually the scroll controller causing this. It is the demand to lay out so many widgets all at once that causes all of these issues, which is being tracked in that other issue I linked. We can leave this open though. Thanks for providing more details.

  8. added
    P2Important issues not at the top of the work list
    on Aug 21, 2024
  9. iapicca commented on Sep 19, 2024

    @iapicca
    Author

    @Piinks the issue does not occur adding prototypeItem
    I'm closing in favor of

    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,
              prototypeItem: MyPrototypeWidget(
                hasIcon: _hasIcon,
                text: '0',
              ),
              itemBuilder: (context, index) => MyPrototypeWidget(
                hasIcon: _hasIcon,
                text: '$index',
              ),
            ),
            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'),
          );
    }
    
    class MyPrototypeWidget extends StatelessWidget {
      final String text;
      final bool hasIcon;
      const MyPrototypeWidget({
        super.key,
        required this.text,
        required this.hasIcon,
      });
    
      @override
      Widget build(context) {
        final textWidget = Text(text, textAlign: TextAlign.center);
        return Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            textWidget,
            for (var i = 0; i < 5; i++) hasIcon ? const FlutterLogo() : textWidget,
          ],
        );
      }
    }
    
  10. github-actions commented on Oct 4, 2024

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  11. locked as resolved and limited conversation to collaborators on Oct 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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