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):


If you modify the code as I described so that when setRowHeights calls resumeUpdates it does not rebuild the heights of all rows, then the problem goes away.

Based on the activity/interest in the flutter issue about the stock ListView.builder performance issue with variable-height rows ( https://github.com/flutter/flutter/issues/52207  ), it seems that fixing this would offer a compelling reason for flutter devs to purchase a license for SfDataGrid, if it can offer fast performance for a grid/list with large lists with variable-height rows. 



KD KD January 28, 2023 06:51 AM UTC

So the suggested fix above isn't quite right, it results in the scrollbar range not getting updated properly so the last few rows don't show when scrolling all the way down.

It appears that the current grid has been optimized for the situation where most rows have the same height and only a few have variable height. In this example, the height of most rows are different from their neighbors, so the range-based approach (DistanceRangeCounterCollection) to map distance information is expensive.

Still, in most cases while scrolling the row sizes are not changing, and so it should only need to do work related to the number of visible rows. I'm motivated to keep trying to help find a way to optimize this without a lot of added complexity and will keep investigating.



KD KD January 29, 2023 07:35 AM UTC

I've been able to find the correct solution and will submit a PR on your github repo soon. There are three issues/optimizations:

When setRowHeights is called within the context of the  _ScrollViewWidgetState _verticalListener, it's better for it to NOT suspendUpdates and then resumeUpdates. resumeUpdates will recalculate the distances for *every* row. It's better

When recalculating the distances, if the default distance and row/line count hasn't changed, it's more efficient NOT to clear the list before rebuilding all the distances.

The red/black tree implementation in the TreeTable had some bugs. It was never setting the color of a new node, so it was forming a red/black tree with the depth equal to the number of nodes! Also, there were some bugs in the insertFixup.

After resolving these, I'm able to scroll a list with 10k variable-height items smoothly with no stutter and it correctly sets the scrollbar ranges on both the top and the bottom (able to fully scroll to the bottom of the last item). With the fixed red/black tree, the depth of the tree was only about 30 instead of 4000, making a huge difference in the lookup and other ops that were being called when row sizes were being updated during scrolling...

I'll submit the PR for consideration by your dev team to incorporate these fixes & optimizations.



KD KD January 31, 2023 06:15 AM UTC

Hi Tamilarasan,

I've finished creating the pull request with the fixes for consideration by your development team.

The details are here:

https://github.com/syncfusion/flutter-widgets/issues/1058

https://github.com/syncfusion/flutter-widgets/pull/1059


You can verify that the fix works for yourself by temporarily putting the following in the pubspec file for the example app above:

  syncfusion_flutter_datagrid:
    git:
      url: https://github.com/klondikedragon/flutter-widgets
      path: packages/syncfusion_flutter_datagrid
      ref: fix/datagrid_scroll_performance


With the fixes in place, SfDataGrid can now smoothly scroll even with 10,000 variable-height rows, and it even works with 100k rows. On these large lists the scrolling performance is 10x or even 100x faster and means the difference between being unusable and being very smooth.

I think many users would appreciate the huge performance boost these fixes offer, so please consider incorporating these fixes into an upcoming release.



AK Ashok Kuvaraja Syncfusion Team February 1, 2023 07:41 AM UTC

Hi KD,

We will validate and update the details within Friday. Thank you for your patience until then.


Regards,

Ashok



TP Tamilarasan Paranthaman Syncfusion Team February 3, 2023 12:45 PM UTC

KD,


We have validated the fix you have provided at our end. We have considered this as a bug and logged a bug report in our feedback portal. We will fix the reported issue and include the changes in our upcoming weekly patch release, which is expected to be rolled out on February 21, 2023. We will let you know once it is published. We appreciate your patience and understanding until then.


Feedback Link:
https://www.syncfusion.com/feedback/40852/the-scrolling-performance-was-degrading-when-loading-a-larger-collection-with



TP Tamilarasan Paranthaman Syncfusion Team February 21, 2023 12:21 PM UTC

KD,


Sorry for the inconvenience caused. We have fixed the reported issue and it’s in the testing phase. We will include the changes in our upcoming weekly patch release which is expected to be rolled out on February 28, 2023. We will update you once the release is rolled out and we appreciate your patience and understanding until then.



TP Tamilarasan Paranthaman Syncfusion Team February 28, 2023 10:51 AM UTC

KD,


Sorry for the inconvenience caused. Still, it’s in the testing phase. We will include the changes in our upcoming weekly patch release which is expected to be rolled out on March 7, 2023. We will update you once the release is rolled out and we appreciate your patience and understanding until then.



TP Tamilarasan Paranthaman Syncfusion Team March 8, 2023 05:27 AM UTC

KD, the fix for the reported issue “The scrolling performance is degrading when autofitting the rows using onQueryRowHeight with large set of rows” has been included in the latest DataGrid version 20.4.53. Update your DataGrid to get the issue resolved.


Loader.
Up arrow icon