Repository: flutter
Issue: itemExtentBuilder: too many unnecessary calls
Labels: framework,c: performance,f: scrolling,has reproducible steps,P3,team-framework,triaged-framework,found in release: 3.19,found in release: 3.20

### Steps to reproduce

itemExtentBuilder is great way to improve app performance. But only when an item height is predefined or easily calculated. But if you need a bit more complex calculations inside the itemExtentBuilder, you will have a performance issue due to many unnecessary itemExtentBuilder calls.

Steps to reproduce the issue. 
1. Use my code example.
2. Scroll to the item... let it say 45.
3. Clear your console.
4. Make two small scrolls.
5. You will see thousands of itemExtentBuilder calls...

In my case itemExtentBuilder was called 10005 times. And itemBuilder only 1. So the question is... are these 10 thousands calls necessary? And that was just a small scroll on the 45th item. Not on 145th one.

### Code sample

<details close><summary>Code sample</summary>

```dart
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

void main() {
  runApp(
    const MaterialApp(
      title: 'Scroll test',
      home: MyApp(),
      initialRoute: '/',
      // showPerformanceOverlay: true,
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return const Directionality(
      textDirection: TextDirection.ltr,
      child: DefaultTextStyle(
        textAlign: TextAlign.left,
        style: TextStyle(
          color: Colors.black54,
          fontFamily: 'Roboto',
          fontSize: 13,
          height: 1.4,
        ),
        child: ScrollTestScreen(),
      ),
    );
  }
}

class ScrollTestScreen extends StatefulWidget {
  const ScrollTestScreen({super.key});
  @override
  State<ScrollTestScreen> createState() => _ScrollTestScreenState();
}

class _ScrollTestScreenState extends State<ScrollTestScreen> {
  int _listLength = 9;
  final int _nextPageTrigger = 3;
  final String _text = 'Lorem ipsum dolor sit amet, consectetur adipiscing '
      'elit, sed do eiusmod tempor incididunt ut labore et dolore magna '
      'aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco '
      'laboris nisi ut aliquip ex ea commodo consequat.';
  final _textStyle = const TextStyle(
    color: Colors.black,
    fontSize: 14,
    height: 1.4,
  );
  final _titleStyle = const TextStyle(
    color: Colors.blueAccent,
    fontSize: 15,
    height: 1.4,
  );

  void _addItems() async {
    if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.idle) {
      await SchedulerBinding.instance.endOfFrame;
    }
    setState(() => _listLength += 5);
  }

  double calcTextHeight(String text, TextStyle style) {
    final textPainter = TextPainter(
      text: TextSpan(text: text, style: style),
      textDirection: TextDirection.ltr,
    )..layout(maxWidth: MediaQuery.of(context).size.width);
    final double height = textPainter.height;
    textPainter.dispose();
    return height;
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      child: Column(children: [
        const SizedBox(height: 60),
        const Text('Simple ListView.builder'),
        const SizedBox(height: 20),
        Expanded(
          child: ListView.builder(
            padding: const EdgeInsets.all(0),
            itemCount: _listLength,
            itemBuilder: (_, index) {
              if (index == _listLength - _nextPageTrigger) _addItems();
              print('item $index');
              return ListItem(index, _text, _textStyle, _titleStyle);
            },
            itemExtentBuilder: (index, _) {
              // It is a known bug https://github.com/flutter/flutter/issues/138912
              if (index >= _listLength) {
                //print('out of range');
                return 100;
              }
              print('extent $index');
              return calcTextHeight('Item number 0', _titleStyle) +
                  calcTextHeight(_text, _textStyle) +
                  // Image height and all sized boxes
                  240;
            },
          ),
        ),
      ]),
    );
  }
}

class ListItem extends StatelessWidget {
  const ListItem(
    this.index,
    this.text,
    this.textStyle,
    this.titleStyle, {
    super.key,
  });
  final int index;
  final String text;
  final TextStyle textStyle;
  final TextStyle titleStyle;

  @override
  Widget build(BuildContext context) {
    return Column(children: [
      Container(
        width: double.infinity,
        height: 200,
        color: Colors.blueGrey,
      ),
      const SizedBox(height: 10),
      Text('Item number $index', style: titleStyle),
      const SizedBox(height: 5),
      Text(text, style: textStyle),
      const SizedBox(height: 25),
    ]);
  }
}

```

</details>


### Performance profiling on master channel

- [X] The issue still persists on the master channel

### Timeline Traces

N/A

### Video demonstration

<details open>
<summary>Video demonstration</summary>

[[Upload media here]](https://github.com/flutter/flutter/assets/9081791/9c299b5f-3f70-4569-b910-dab68a510121)

</details>




### What target platforms are you seeing this bug on?

iOS

### OS/Browser name and version | Device information

iOS simulator: iPhone 15 Pro

### Does the problem occur on emulator/simulator as well as on physical devices?

Yes

### Is the problem only reproducible with Impeller?

N/A

### Logs

_No response_

### Flutter Doctor output

<details open><summary>Doctor output</summary>

```console
[✓] Flutter (Channel master, 3.18.0-14.0.pre.14, on macOS 14.2.1 23C71 darwin-arm64 (Rosetta), locale
    XX)
    • Flutter version 3.18.0-14.0.pre.14 on channel master at /Users/XXXXX/Development/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision bbfe177dc9 (2 months ago), 2023-12-19 05:48:01 -0500
    • Engine revision 1d5a141917
    • Dart version 3.3.0 (build 3.3.0-241.0.dev)
    • DevTools version 2.31.0-dev.0

[✓] Android toolchain - develop for Android devices (Android SDK version 32.1.0-rc1)
    • Android SDK at /Users/Anton/Library/Android/sdk
    • Platform android-32, build-tools 32.1.0-rc1
    • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 15.2)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 15C500b
    • CocoaPods version 1.14.3

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2021.2)
    • 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 11.0.12+0-b1504.28-7817840)

[✓] VS Code (version 1.86.0)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.82.0

[✓] Connected device (4 available)
    • iPhone XXXXX (mobile)  • 00008110-000A70163682801E            • ios            • iOS 17.3.1
      21D61
    • iPhone 15 Pro (mobile) • B5803A97-0140-4C01-8C0D-6731611F7A73 • ios            •
      com.apple.CoreSimulator.SimRuntime.iOS-17-0 (simulator)
    • macOS (desktop)        • macos                                • darwin-arm64   • macOS 14.2.1
      23C71 darwin-arm64 (Rosetta)
    • Chrome (web)           • chrome                               • web-javascript • Google Chrome
      122.0.6261.94

[✓] Network resources
    • All expected network resources are available.

• No issues found!
```

</details>