We use cookies to give you the best experience on our website. If you continue to browse, then you agree to our privacy policy and cookie policy. Image for the cookie policy date

Variable height rows very slow with 1k+ rows, unusable with 10k+ rows

I have a SfDataGrid with two columns, the first is small and fixed size, the second is variable sized text and may wrap. I'm fitting the row height based on the text content of rows based on the example in the documentation.

If there are lots of rows (e.g., 1000) then as you scroll farther down the list it gets very slow (multiple seconds between display updates). And if you try 10k rows it's completely unusable. The farther you scroll down the list the slower it gets.

(Just a theory: maybe there is some O(N^2) algorithm if it's trying to get the vertical scroll position of an item, it has to traverse the array holding the row sizes of all previous items. Instead, it could potentially cache the row sizes of all previous items for every item or every Nth item.)

If there are fixed-sized rows instead then even 100k rows in a data grid is fast and works great. It's only the use of onQueryRowHeight that causes performance to get very slow and unusable.

Here is a code snippet with the example, it's just randomly generating data in this case:

class LargeListDataPoint {
  final DateTime dt;
  final String message;
  const LargeListDataPoint({
    required this.dt,
    required this.message,
  });
}

class LargeListDataGridRow extends DataGridRow {
  final LargeListDataPoint data;
  const LargeListDataGridRow({required super.cells, required this.data});
}

class LargeListDataSource extends DataGridSource {
  static const columnNameDateTime = 'datetime';
  static const columnNameMessage = 'message';

  late final List _rows;
  LargeListDataSource(int desiredRows) {
    final rnd = Random();
    _rows = List.generate(desiredRows, (index) {
      final data = LargeListDataPoint(
        severity:
            SeverityGroups.values[rnd.nextInt(SeverityGroups.values.length)],
        dt: DateTime.now().toUtc(),
        message: List.generate(rnd.nextInt(40), (_) => rnd.nextInt(1 << 31))
            .join(" "),
      );
      return LargeListDataGridRow(
        data: data,
        cells: [
          DataGridCell(
              columnName: columnNameDateTime, value: data.dt.toIso8601String()),
          DataGridCell(columnName: columnNameMessage, value: data.message),
        ],
      );
    }).toList();
  }

  @override
  List get rows => _rows;

  @override
  DataGridRowAdapter? buildRow(DataGridRow row) {
    return DataGridRowAdapter(
        cells: row.getCells().map((cell) {
      final child = () {
        switch (cell.columnName) {
          case columnNameDateTime:
          case columnNameMessage:
            final text = (cell.value ?? "").toString();
            return Padding(
                padding: const EdgeInsets.only(left: 4.0, right: 4.0),
                child: Text(text));
          default:
            return null;
        }
      }();
      return Container(child: child);
    }).toList());
  }
}

class TestLargeListWidget extends HookWidget {
  const TestLargeListWidget({super.key});

  @override
  Widget build(BuildContext context) {
    const kNumTestItems = 10000;
    final source = useMemoized(
        () => LargeListDataSource(kNumTestItems), [kNumTestItems]);
    return SfDataGrid(
      source: source,
      onQueryRowHeight: (details) {
        return details.getIntrinsicRowHeight(
          details.rowIndex,
          excludedColumns: [
            LargeListDataSource.columnNameDateTime,
          ],
        );
      },
      allowColumnsResizing: false,
      shrinkWrapColumns: false,
      shrinkWrapRows: false,
      headerRowHeight: 25,
      //rowHeight: 32,
      columnWidthMode: ColumnWidthMode.lastColumnFill,
      columns: [
        GridColumn(
          columnName: LargeListDataSource.columnNameDateTime,
          label: const Text("Date/Time"),
        ),
        GridColumn(
          columnName: LargeListDataSource.columnNameMessage,
          label: const Text("Message"),
        ),
      ],
    );
  }
}



11 Replies

KD KD January 27, 2023 04:43 AM UTC

Doing some profiling of the code, it looks like that by changing just a few lines of code, the performance of an SfDataGrid with even 100,000 rows is extremely good! (snappy, no delay when scrolling to any point in the list)

The profiling shows that the problem is that it's recalulating the line height information for *all* rows in the list whenever the scroll position changes, even though it's only recalculating the row height for the small range of visible rows during scrolling.

Here is the control flow:

When the scrollbar position changes, the _verticalListener is called inside of _ScrollViewWidgetState. This calls _container.setRowHeights().

Inside of setRowHeights, it's calling lineSizeCollection.suspendUpdates(); (presumably to prevent the view from getting rebuilt as each recalculated row height changes?)

It then proceeds to update the row height information for the currently visible rows, as well as for header/footer rows.

Then, it calls lineSizeCollection.resumeUpdates(); This is where the problem is. Inside of resumeUpdates, the suspend counter reaches zero, and it calls initializeDistances(); Inside of initializeDistances, it clears all previous height information, and recalulates the distances for every line (if you're scrolling down farther into the list, this will be thousands or tens of thousands of rows!).

The solution I tried out was to change the resumeUpdates function to take a new parameter to indicate if all distances should be recalculated as part of the resume, defaulting to the old behavior:

void resumeUpdates({bool reinitDistances = true}) {


Then, only call initializeDistances if the parameter is true:

if (_distances != null && reinitDistances) {


Finally, inside of setRowHeights, when it calls lineSizeCollection.resumeUpdates, tell it not to calculate the distances for all rows:

lineSizeCollection.resumeUpdates(reinitDistances: false);


With these changes it can instantly scroll anywhere in the list using the mouse or by dragging it, even with 100,000 rows in the list or more. This shows that the SfDataGrid is very close to being able to support large lists with variable sized heights with just a little tweaking. Please consider and fix.



TP Tamilarasan Paranthaman Syncfusion Team January 27, 2023 12:35 PM UTC

Hi KD,


We have been able to reproduce the issue on our end, and we have also found that it occurs in the ListView.builder sample when it contains a large number of records. The problem is caused by the dynamic creation of tiles with respective heights, just as the DataGrid does while performing vertical scrolling. We have reported this issue to the Flutter framework team, and they have acknowledged it as a known issue. They are currently working on a solution and once it is fixed, the performance will improve. For more information, you can refer to the following GitHub link: https://github.com/flutter/flutter/issues/114809


As a temporary solution, we recommend setting the SfDataGrid.shrinkWrapRows property to true. This will calculate the entire height of the DataGrid at initial loading, which will improve the scrolling performance compared to the current performance. Additionally, the entire application, including the header, will scroll. Please refer to the following code snippet for an example of how to implement this:


@override

  Widget build(BuildContext context) {

    return SingleChildScrollView(

        child: SfDataGrid(

          shrinkWrapRows: true,

          source: _employeeDataSource,

          columns: getColumns,

        ),

      );

  }


We have provided detailed information in the below UG documentation. Please go through this.


UG Document: https://help.syncfusion.com/flutter/datagrid/scrolling#set-height-and-width-of-datagrid-based-on-rows-and-columns-available


Regards,

Tamilarasan



KD KD replied to Tamilarasan Paranthaman January 28, 2023 05:02 AM UTC

Thanks for the response. However, using shrinkWrapRows: true inside of a SingleChildScrollView is not working well. With 10,000 items, with the app built in release mode, it is taking a full 5 seconds when the widget is first built before it shows anything (the application is completely hung during that time). Once the page loads, scrolling is also still slow although a faster than before. Additionally, scrolling the header is also not ideal.

The flutter issue that you linked to does not appear to be directly related. It's related to the standard ListView.builder in flutter not having good performance with variable-height lists because it recalculates the height of everything. In this case, based on the code of SfDataGrid, your data grid widget already has sophisticated logic to avoid recalculating the heights of everything by caching row height information. However, there appears to be a bug in the logic where it is improperly causing all row heights to be recalculated every scroll event. With the fix that I mentioned above, the SfDataGrid is able to scroll nearly instantly even with 100,000 items in the list (not possible with shrinkWrapRows: true approach).

Here is the output of the flutter devtools CPU profiling flamegraph when trying to scroll with bug present. You can see that nearly all CPU time is spent inside of the LineSizeCollection.resumeUpdates function calling the LineSizeCollection.initializeDistances function, which recalculates the row heights for all rows (the larger the list, the worse it gets):